Merge branch 'develop' into feature/jnou/fix-iss-regression

This commit is contained in:
JennyNou 2026-02-13 10:00:22 -05:00
commit 9835de34c7
20 changed files with 1408 additions and 2105 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View file

@ -93,6 +93,10 @@ const endpoints = Object.freeze({
) => `${LOCATION_BASE_URL}/providers/${zipCode}/${damageType}/${radius}/${parentAccountNumber}/${safeliteOnly}/${carId}/${recalPartNumber != null ? `${recalPartNumber}/` : ''}${isBigTruck}`, ) => `${LOCATION_BASE_URL}/providers/${zipCode}/${damageType}/${radius}/${parentAccountNumber}/${safeliteOnly}/${carId}/${recalPartNumber != null ? `${recalPartNumber}/` : ''}${isBigTruck}`,
method: 'GET' method: 'GET'
}, },
GetTpaAndSafeliteProviders: {
url: `${LOCATION_BASE_URL}/tpa-and-safelite-providers`,
method: 'GET'
},
GetCapabilityQuestions: { GetCapabilityQuestions: {
url: `${PARTS_BASE_URL}/capability-questions`, url: `${PARTS_BASE_URL}/capability-questions`,
method: 'GET' method: 'GET'

View file

@ -67,7 +67,10 @@ const errorMessages = Object.freeze({
DATE_REQUIRED: 'Please select a date', DATE_REQUIRED: 'Please select a date',
TIME_REQUIRED: 'Please select an appointment time.', TIME_REQUIRED: 'Please select an appointment time.',
PRIMARY_PHONE_REQUIRED: 'Primary phone number is required.', PRIMARY_PHONE_REQUIRED: 'Primary phone number is required.',
BAILOUT_EMAIL_ADDRESS_REQUIRED: 'Email address is required.' BAILOUT_EMAIL_ADDRESS_REQUIRED: 'Email address is required.',
TPA_SEARCH_ZIP_FORMAT: 'Please enter a valid Zip code.',
TPA_SEARCH_SHOP_FORMAT: 'Please enter a valid shop name.',
TPA_SEARCH_REQUIRED: 'Please enter a shop name or ZIP code.'
}); });
export default errorMessages; export default errorMessages;

View file

@ -28,7 +28,8 @@ const globalRules = Object.freeze({
ZIP_CODE_SEARCH_FORMAT: 'zip-code-search-format', ZIP_CODE_SEARCH_FORMAT: 'zip-code-search-format',
OPTION_REQUIRED: 'option-required', OPTION_REQUIRED: 'option-required',
PRIMARY_PHONE_REQUIRED: 'primary-phone-required', PRIMARY_PHONE_REQUIRED: 'primary-phone-required',
BAILOUT_EMAIL_ADDRESS_REQUIRED: 'bailout-email-address-required' BAILOUT_EMAIL_ADDRESS_REQUIRED: 'bailout-email-address-required',
TPA_SEARCH_FORMAT: 'tpa-search-format'
}); });
export default globalRules; export default globalRules;

View file

@ -1,91 +0,0 @@
// TintMap with array keys by location, lowercase to avoid as much string mismatching as possible.
// Src assumes you have a @/assets/img/tints/, making the final value @/assets/img/tints/{Src} in the markup.
// See vehicle-parts for implementation example.
const tintMap = Object.freeze({
other: [
// Blue Shade
{ name: 'blue tint, blue shade', src: 'Glass-BlueShade-BlueTint.svg' },
{ name: 'brown tint, blue shade', src: 'Glass-BlueShade-BrownTint.svg' },
{ name: 'gray tint, blue shade', src: 'Glass-BlueShade-GrayTint.svg' },
{ name: 'green tint, blue shade', src: 'Glass-BlueShade-GreenTint.svg' },
{ name: 'clear, blue shade', src: 'Glass-BlueShade-NoTint.svg' },
// Brown Shade
{ name: 'brown tint, brown shade', src: 'Glass-BrownShade-BrownTint.svg' },
// Gray Shade
{ name: 'blue tint, gray shade', src: 'Glass-GrayShade-BlueTint.svg' },
{ name: 'brown tint, gray shade', src: 'Glass-GrayShade-BrownTint.svg' },
{ name: 'gray tint, gray shade', src: 'Glass-GrayShade-GrayTint.svg' },
{ name: 'green tint, gray shade', src: 'Glass-GrayShade-GreenTint.svg' },
// Green Shade
{ name: 'blue tint, green shade', src: 'Glass-GreenShade-BlueTint.svg' },
{ name: 'brown tint, green shade', src: 'Glass-GreenShade-BrownTint.svg' },
{ name: 'green tint, green shade', src: 'Glass-GreenShade-GreenTint.svg' },
// Tints Only
{ name: 'blue tint privacy', src: 'Glass-NoShade-BlueTint.svg' },
{ name: 'bronze tint', src: 'Glass-NoShade-BrownTint.svg' },
{ name: 'dark brown tint', src: 'Glass-NoShade-DarkBrownTint.svg' },
{ name: 'privacy, black frame', src: 'Glass-NoShade-Privacy.svg' },
{ name: 'gray tint', src: 'Glass-NoShade-GrayTint.svg' },
{ name: 'green tint', src: 'Glass-NoShade-GreenTint.svg' },
{ name: 'gray tint privacy', src: 'Glass-NoShade-GrayTint.svg' },
{ name: 'blue tint', src: 'Glass-NoShade-BlueTint.svg' },
{ name: 'green tint privacy', src: 'Glass-NoShade-GreenTint.svg' },
// No shade or tint
{ name: 'clear', src: 'Glass-NoShade-NoTint.svg' }
],
windshield: [
// Blue Shade
{ name: 'blue tint, blue shade', src: 'Windshield-BlueShade-BlueTint.svg' },
{ name: 'bronze tint, blue shade', src: 'Windshield-BlueShade-BrownTint.svg' },
{ name: 'gray tint, blue shade', src: 'Windshield-BlueShade-GrayTint.svg' },
{ name: 'green tint, blue shade', src: 'Windshield-BlueShade-GreenTint.svg' },
{ name: 'clear, blue shade', src: 'Windshield-BlueShade-NoTint.svg' },
// Brown Shade
{ name: 'bronze tint, bronze shade', src: 'Windshield-BrownShade-BrownTint.svg' },
// Gray Shade
{ name: 'blue tint, gray shade', src: 'Windshield-GrayShade-BlueTint.svg' },
{ name: 'brown tint, gray shade', src: 'Windshield-GrayShade-BrownTint.svg' },
{ name: 'gray tint, gray shade', src: 'Windshield-GrayShade-GrayTint.svg' },
{ name: 'green tint, gray shade', src: 'Windshield-GrayShade-GreenTint.svg' },
// Green Shade
{ name: 'blue tint, green shade', src: 'Windshield-GreenShade-BlueTint.svg' },
{ name: 'brown tint, green shade', src: 'Windshield-GreenShade-BrownTint.svg' },
{ name: 'green tint, green shade', src: 'Windshield-GreenShade-GreenTint.svg' },
// Tints Only
{ name: 'blue tint', src: 'Windshield-NoShade-BlueTint.svg' },
{ name: 'bronze tint', src: 'Windshield-NoShade-BrownTint.svg' },
{ name: 'dark brown tint', src: 'Windshield-NoShade-DarkBrownTint.svg' },
{ name: 'privacy, black frame', src: 'Windshield-NoShade-Privacy.svg' },
{ name: 'gray tint', src: 'Windshield-NoShade-GrayTint.svg' },
{ name: 'green tint', src: 'Windshield-NoShade-GreenTint.svg' },
{ name: 'gray tint privacy', src: 'Windshield-NoShade-GrayTint.svg' },
// No shade or tint
{ name: 'clear', src: 'Windshield-NoShade-NoTint.svg' }
]
});
// Gets the tint image source string given the glass location, and the tint description (like 'Green Tint')
// Use lowered strings here to try to avoid mismatch. Returns an empty string if array key doesn't exist.
// Returns undefined if no items are found.
const getTintImage = (glassLocation, colorString) => {
// Windshield glass has special images, all other glass uses the same though.
const glassLoc = (glassLocation.toLowerCase() !== 'windshield') ? 'other' : 'windshield';
if (tintMap[glassLoc] === undefined) {
return '';
}
return tintMap[glassLoc].find((item) => item.name.toLowerCase() === colorString.toLowerCase());
};
export default getTintImage;

View file

@ -310,22 +310,31 @@ input::-webkit-date-and-time-value {
.input-wrapper { .input-wrapper {
position: relative; position: relative;
&.has-search-icon { &.has-search-icon {
display:flex;
.form-control {
&:focus, &:focus-within {
border: 1px solid #0070d1;
}
max-height: 3rem;
}
input[type='text'] { input[type='text'] {
border-radius: $border-radius; border-color: #0070d1;
box-shadow: rgba(0, 0, 0, 0.2) 0rem 0.0625rem .25rem 0rem;
border-right: none;
border-radius: 4.5rem 0 0 4.5rem;
height: 3rem;
&:focus, &:focus-within {
border-right:none;
}
} }
button[type='submit'] { button[type='submit'] {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 0;
background-image: url($svg-search-icon); background-image: url($svg-search-icon);
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center; background-position: center;
border-radius: 0 $border-radius-lg $border-radius-lg 0; border-radius: 0 4.5rem 4.5rem 0;
background-color: $blue-100; background-color: $blue-100;
width: 2.75rem; width: 2.75rem;
height: 100%; border: 1px solid #0070d1;
border: 1px solid $gray-500;
border-left: none; border-left: none;
display: flex; display: flex;
} }

View file

@ -125,6 +125,28 @@ function defineGlobalZipCodeRules() {
defineRule(globalRules.ZIP_CODE_SEARCH_FORMAT, regex(/^\d{5}$/, errorMessages.ZIP_FORMAT)); defineRule(globalRules.ZIP_CODE_SEARCH_FORMAT, regex(/^\d{5}$/, errorMessages.ZIP_FORMAT));
} }
/**
* @summary Define global rules related to the zip code/shop name search on TPA Search page
*/
function defineGlobalTpaSearchRules() {
defineRule(globalRules.TPA_SEARCH_FORMAT, (value) => {
if (value.length === 0) {
return errorMessages.TPA_SEARCH_REQUIRED;
}
// Non Numeric
if (isNaN(value)) {
if (value.length < 3) {
return errorMessages.TPA_SEARCH_SHOP_FORMAT;
}
} else {
if (value.length != 5) {
return errorMessages.TPA_SEARCH_ZIP_FORMAT;
}
}
return true;
});
}
/** /**
* @function defineGlobalRules * @function defineGlobalRules
* @summary Define all global rules * @summary Define all global rules
@ -138,6 +160,6 @@ export default function defineGlobalRules() {
defineGlobalPolicyNumberRules(); defineGlobalPolicyNumberRules();
defineGlobalPolicyZipCodeRules(); defineGlobalPolicyZipCodeRules();
defineGlobalZipCodeRules(); defineGlobalZipCodeRules();
defineGlobalTpaSearchRules();
defineRule(globalRules.OPTION_REQUIRED, required(errorMessages.OPTION_REQUIRED)); defineRule(globalRules.OPTION_REQUIRED, required(errorMessages.OPTION_REQUIRED));
} }

View file

@ -46,7 +46,7 @@ export function createUnorderedListFromStringOfParagraphs(stringOfParagraphs) {
*/ */
export function toTitleCase(text) { export function toTitleCase(text) {
let temp = text?.toLowerCase() ?? ''; let temp = text?.toLowerCase() ?? '';
return temp.replace(/(^|\s|-)\S/g, (letter) => letter.toUpperCase()); return temp.replace(/(^|\s|-|\/)\S/g, (letter) => letter.toUpperCase());
} }
/** /**

View file

@ -6,7 +6,7 @@
<div class="iss-heritage-container-width"> <div class="iss-heritage-container-width">
<div class="questions-page-layout-container iss-heritage-content-container-width"> <div class="questions-page-layout-container iss-heritage-content-container-width">
<siteSubHeader <siteSubHeader
class="mt-4" class="mt-4 subheader"
cmsWidgetName="SiteSubHeaderWidget" /> cmsWidgetName="SiteSubHeaderWidget" />
<alert <alert
v-if="alertFewMoreQuestionsHeader || alertFewMoreQuestionsCopy" v-if="alertFewMoreQuestionsHeader || alertFewMoreQuestionsCopy"
@ -129,6 +129,13 @@ export default {
padding-left: .9375rem; padding-left: .9375rem;
padding-right: .9375rem; padding-right: .9375rem;
} }
.subheader {
:deep(strong) {
color: $black;
font-weight: 600;
text-transform: uppercase;
}
}
} }
.questions-page { .questions-page {
@ -141,6 +148,7 @@ export default {
} }
} }
.need-help-link-container { .need-help-link-container {
margin-top: 1.875rem;
margin-bottom: 2.25rem; margin-bottom: 2.25rem;
} }
</style> </style>

View file

@ -14,14 +14,13 @@
id="stacked" id="stacked"
class="col button-col d-flex px-0"> class="col button-col d-flex px-0">
<buttonMain <buttonMain
v-if="!isForwardButtonHidden"
ref="buttonMain" ref="buttonMain"
variant="navigation" variant="navigation"
:buttonText="buttonText" :buttonText="buttonText"
:class=" :class="{
(disableForwardAction || isForwardActionDisabled) && 'form-test-invalid': (disableForwardAction || isForwardActionDisabled),
'form-test-invalid' 'hide-button': isForwardButtonHidden
" }"
:aria-disabled="disableForwardAction || isForwardActionDisabled" :aria-disabled="disableForwardAction || isForwardActionDisabled"
:isDisabled="disableForwardAction || isForwardActionDisabled" :isDisabled="disableForwardAction || isForwardActionDisabled"
data-test-id="site-footer-main-button" data-test-id="site-footer-main-button"
@ -149,5 +148,8 @@ export default {
margin-top: 1.25rem; margin-top: 1.25rem;
} }
} }
.hide-button {
visibility: hidden;
}
} }
</style> </style>

View file

@ -0,0 +1,119 @@
<template>
<transition
name="fade"
mode="out-in">
<baseInputButton
v-bind="$props"
v-model="selectedValue"
buttonWrapperClasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 label-margin-bottom no-hover">
<div
: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
id="buttonLabelSpan"
class="m-0 button-label-copy"
:class="textPosition">{{ buttonLabel }}
</span>
<span
id="buttonLabelSubCopySpan"
class="m-0 caption ms-2"
:class="textPosition">{{ buttonLabelSubCopy }}
</span>
</div>
<div class="row-two">
<span
v-if="buttonBodyCopy"
id="buttonBodyCopy"
class="m-0 button-label-sub-copy"
v-html="buttonBodyCopy"></span>
</div>
<span
v-if="screenReaderOnlyText"
id="screenReaderOnlyTextSpan"
class="sr-only">
{{ screenReaderOnlyText }}
</span>
</div>
</baseInputButton>
</transition>
</template>
<script>
import baseInputButton from '@/digital-components/base-input-button/base-input-button.vue';
import inputButtonWrapperMixin from '@/mixins/input-button-wrapper-mixin';
import loader from '@/ux-components/loader/loader.vue';
export default {
name: 'tpa-shop-list-button',
components: {
baseInputButton,
loader
},
mixins: [inputButtonWrapperMixin],
};
</script>
<style lang="scss" scoped>
@import "@/styles/ux-variables-svg-strings.scss";
.list-button {
outline: none;
input[type="radio"],
input[type="checkbox"] {
position: static; //override bootstrap
&:checked + .list-button-content {
background: #e7f1f6;
border: 1px solid #0070d1;
}
}
}
.list-button-content {
color: $gray-600;
position: relative;
background: $white;
transition: all 150ms linear;
border-radius: $border-radius-lg;
border: 1px solid $gray-500;
width: 100%;
outline: none;
span {
&.small {
font-size: 0.75rem;
color: $gray-550;
}
}
}
.button-content {
row-gap: 0.25rem;
color: #4d4e53;
font-size: .9375rem;
font-weight: 400;
line-height: 1.625rem;
.row-one {
display: flex;
align-items: center;
line-height: 1.5rem;
.button-label-copy {
font-weight: 600;
line-height: 1.5rem;
text-transform: capitalize;
}
#buttonLabelSubCopySpan {
font-size: .75rem;
line-height: 1.5rem;
}
}
.row-two {
text-align: left;
}
}
div.col:not(:last-child) >.label-margin-bottom{
margin-bottom: 1rem;
}
</style>

View file

@ -1,214 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TPA search page returns the initial data 1`] = `
Object {
"additionalButtonData": Object {
"displayAvailabilityIndicators": false,
},
"dataLoaded": false,
"filter": "",
"mapZipCode": null,
"providers": Array [],
"reloadingProviders": false,
"rules": Object {
"filter": "option-required",
"provider": "option-required",
"zipCode": "zip-code-required|zip-code-search-format",
},
"selectedProviderNumber": "",
"shopListButton": Object {
"beforeMount": [Function],
"components": Object {
"baseInputButton": Object {
"computed": Object {
"buttonId": [Function],
"eventTypes": [Function],
"inputType": [Function],
"isChecked": [Function],
"isValueSelectedOnClick": [Function],
},
"data": [Function],
"emits": Array [
"update:modelValue",
],
"methods": Object {
"handleBlur": [Function],
"handleClick": [Function],
"handleEventAction": [Function],
"handleFocus": [Function],
"handleSelectionChange": [Function],
},
"mounted": [Function],
"name": "base-input-button",
"props": Object {
"buttonWrapperClasses": Array [
[Function],
[Function],
[Function],
],
"groupName": Object {
"required": true,
"type": [Function],
},
"inputClasses": Array [
[Function],
[Function],
[Function],
],
"isMultiSelect": [Function],
"isRequired": Object {
"default": true,
"type": [Function],
},
"lastValuePushedToGa": Array [
[Function],
[Function],
],
"modelValue": Object {
"required": true,
"validator": [Function],
},
"selectingInitiatesLoad": Object {
"default": false,
"type": [Function],
},
"setLastValuePushedToGa": [Function],
"suppressError": [Function],
"validationRules": Object {
"default": "",
"type": [Function],
},
"value": Object {
"required": true,
"type": Array [
[Function],
[Function],
],
},
"valueToLogType": [Function],
},
"render": [Function],
"setup": [Function],
},
"loader": Object {
"computed": Object {
"cssProps": [Function],
},
"methods": Object {
"captureClick": [Function],
},
"name": "loader",
"props": Object {
"allowPageInteraction": Object {
"default": false
"type": [Function],
},
"height": Object {
"default": 1,
"type": [Function],
},
"loaderColor": Object {
"type": [Function],
},
"loaderPosition": Object {
"type": [Function],
},
"width": Object {
"default": 1,
"type": [Function],
},
},
"render": [Function],
},
},
"computed": Object {
"availabilityRatingClass": [Function],
"badgeText": [Function],
"isLoaderDisplayed": [Function],
},
"data": [Function],
"mixins": Array [
Object {
"computed": Object {
"selectedValue": Object {
"get": [Function],
"set": [Function],
},
},
"model": Object {
"event": "change",
"prop": "modelValue",
},
"props": Object {
"additionalButtonData": [Function],
"additionalButtonStyling": [Function],
"altText": Object {
"default": "",
"type": [Function],
},
"buttonAuxiliaryCopy": [Function],
"buttonBodyCopy": [Function],
"buttonFooterCopy": [Function],
"buttonImage": [Function],
"buttonImageId": [Function],
"buttonLabel": Array [
[Function],
[Function],
],
"buttonLabelSubCopy": [Function],
"groupName": Object {
"required": true,
"type": [Function],
},
"isMultiSelect": [Function],
"isRequired": Object {
"default": true,
"type": [Function],
},
"isWide": [Function],
"lastValuePushedToGa": Array [
[Function],
[Function],
],
"modelValue": Object {
"required": true,
"validator": [Function],
},
"screenReaderOnlyText": [Function],
"selectingInitiatesLoad": Object {
"default": false,
"type": [Function],
},
"setLastValuePushedToGa": [Function],
"suppressError": [Function],
"textPosition": [Function],
"validationRules": Object {
"default": "",
"type": [Function],
},
"value": Object {
"required": true,
"type": Array [
[Function],
[Function],
],
},
"valueToLogType": [Function],
},
},
],
"name": "shop-list-button",
"render": [Function],
},
"widget": Object {
"filterByQuestion": "FilterByQuestion",
"noNetworkShopsAlert": "NoNetworkShopsAlertWidget",
"searchInstructions": "SearchInstructions",
"shopNotListedLink": "ShopNotListedLink",
"siteFooter": "SiteFooterWidget",
"siteHeader": "SiteHeaderWidget",
"tpaSearchQuestion": "TPASearchQuestion",
},
"zipCode": null,
}
`;

File diff suppressed because it is too large Load diff

View file

@ -15,26 +15,26 @@
<label <label
id="tpaSearchQuestionLabel" id="tpaSearchQuestionLabel"
for="tpaSearchQuestionField" for="tpaSearchQuestionField"
class="text-center fs-5 mt-5 mb-0 text-black w-100"> class="fs-5 mt-5 text-black w-100">
{{ tpaSearchQuestionLabel }} {{ tpaSearchQuestionLabel }}
</label> </label>
<label <label
id="searchInstructions" id="searchInstructions"
for="tpaSearchQuestionField" for="tpaSearchQuestionField"
class="text-center small darker-gray w-100 mb-4"> class="darker-gray w-100 mb-4">
{{ searchInstructionsText }} {{ searchInstructionsText }}
</label> </label>
<div class="mb-5"> <div class="mb-5">
<textboxQuestion <textboxQuestion
id="tpaSearchQuestionField" id="tpaSearchQuestionField"
v-model="zipCode" v-model="tpaSearchValue"
inputId="tpaSearchQuestionFieldInput" inputId="tpaSearchQuestionFieldInput"
:cmsWidgetName="widget.tpaSearchQuestion" :cmsWidgetName="widget.tpaSearchQuestion"
:includeSearchIcon="true" :includeSearchIcon="true"
:displayQuestionText="false" :displayQuestionText="false"
isRequired isRequired
:isDisabled="reloadingProviders" :isDisabled="reloadingProviders"
:validationRules="rules.zipCode" :validationRules="rules.tpaSearch"
@clickEvent="searchClick" /> @clickEvent="searchClick" />
</div> </div>
</Form> </Form>
@ -52,10 +52,10 @@
@invalidSubmit="onInvalidSubmit"> @invalidSubmit="onInvalidSubmit">
<dropdownQuestion <dropdownQuestion
id="searchRadiusFilter" id="searchRadiusFilter"
v-model="filter" v-model="radiusFilter"
:cmsWidgetName="widget.filterByQuestion" :cmsWidgetName="widget.filterByQuestion"
inputId="searchRadiusFilterInput" inputId="searchRadiusFilterInput"
:options="filterOptions" :options="radiusFilterOptions"
disableAutoFill disableAutoFill
:isDisabled="reloadingProviders" :isDisabled="reloadingProviders"
:validationRules="rules.filter" /> :validationRules="rules.filter" />
@ -73,7 +73,7 @@
id="selectProviderQuestion" id="selectProviderQuestion"
v-model="selectedProviderNumber" v-model="selectedProviderNumber"
buttonTypeString="shopListButton" buttonTypeString="shopListButton"
:buttonTypeObject="shopListButton" :buttonTypeObject="tpaShopListButton"
class="radioQuestion" class="radioQuestion"
:answers="providerButtonData" :answers="providerButtonData"
groupName="chooseShop" groupName="chooseShop"
@ -99,12 +99,23 @@
:text="shopNotListedModalLink" :text="shopNotListedModalLink"
@clickEvent="doNotSeeMyShopLinkClick" /> @clickEvent="doNotSeeMyShopLinkClick" />
</div> </div>
<hr>
<siteFooter <siteFooter
class="mt-6"
ref="siteFooter" ref="siteFooter"
:cmsWidgetName="widget.siteFooter" :cmsWidgetName="widget.siteFooter"
:isForwardActionDisabled="!meta.valid" :isForwardActionDisabled="!meta.valid"
@ForwardClicked="forwardButtonAction" @ForwardClicked="forwardButtonAction"
@backClicked="navigateBack" /> @backClicked="navigateBack" />
<div class="need-help-link-container">
<textLink
id="needHelpLink"
v-if="needHelpLinkText"
linkType="navigation"
:text="needHelpLinkText"
href="#"
@clickEvent="needHelpLinkClick" />
</div>
</div> </div>
</Form> </Form>
</div> </div>
@ -123,7 +134,7 @@ import textLink from '@/ux-components/text-link/text-link.vue';
import alert from '@/ux-components/alert/alert.vue'; import alert from '@/ux-components/alert/alert.vue';
import siteFooter from '@/iss-components/site-footer/site-footer.vue'; import siteFooter from '@/iss-components/site-footer/site-footer.vue';
import googleMap from '@/iss-components/google-map/google-map.vue'; import googleMap from '@/iss-components/google-map/google-map.vue';
import shopListButton from '@/iss-components/shop-list-button/shop-list-button.vue'; import tpaShopListButton from '@/iss-components/tpa-shop-list-button/tpa-shop-list-button.vue';
import loader from '@/ux-components/loader/loader.vue'; import loader from '@/ux-components/loader/loader.vue';
// Supporting files // Supporting files
@ -135,53 +146,9 @@ import widgetFields from '@/constants/cms-widget-fields.js';
import { shallowRef } from 'vue'; import { shallowRef } from 'vue';
import { toTitleCase, toDisplayPhoneNumber } from '@/helpers/text-helper.js'; import { toTitleCase, toDisplayPhoneNumber } from '@/helpers/text-helper.js';
import bailoutMessage from '@/constants/bailoutMessage'; import bailoutMessage from '@/constants/bailoutMessage';
import settleAllPromises from '@/helpers/layout-helper';
import issPageValues from '@/router/router-constants/issPage-values';
const radiusFilterPairs = [
{ radius: 15, filter: '15 miles' },
{ radius: 25, filter: '25 miles' },
{ radius: 50, filter: '50 miles' },
{ radius: 100, filter: '100 miles' }
];
function convertRadiusFilterToInteger(filter) {
const pair = radiusFilterPairs.find((p) => p.filter === filter);
return pair?.radius ?? 0;
}
async function getInitialSearchData() {
const customerZipCode = useMainStore().order.customer.address.zipCode;
const serviceLocationZipCode = useMainStore().order.serviceLocation.zipCode;
const zipCode = serviceLocationZipCode ?? customerZipCode;
const { searchFilter } = useMainStore().order.serviceLocation;
let { providerNumber } = useMainStore().order.serviceLocation.provider;
let providers = [];
let radius = 0;
if (searchFilter) {
radius = convertRadiusFilterToInteger(searchFilter);
const result = await useMainStore().getTpaProviders(zipCode, radius);
providers = result?.data?.shopProviders ?? [];
return { zipCode, filter: searchFilter, providers, providerNumber };
}
providerNumber = null;
let pairIndex = 0;
let filter = '';
while (providers.length === 0 && pairIndex < radiusFilterPairs.length) {
const pair = radiusFilterPairs[pairIndex];
radius = pair.radius;
filter = pair.filter;
// eslint-disable-next-line no-await-in-loop
const result = await useMainStore().getTpaProviders(zipCode, radius);
providers = result?.data?.shopProviders ?? [];
pairIndex += 1;
}
return { zipCode, filter, providers, providerNumber };
}
export default { export default {
name: 'tpa-search', name: 'tpa-search',
@ -200,23 +167,40 @@ export default {
}, },
mixins: [BaseFormMixin], mixins: [BaseFormMixin],
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
const { zipCode, filter, providers, providerNumber } = const zipCode = useMainStore().order.customer.address.zipCode;
await getInitialSearchData(); const pageData = useMainStore().pageData(to.query.issPage);
const cmsContent = await fetchCmsContentForPage(to.query.issPage); const cmsContentPromise = fetchCmsContentForPage(to.query.issPage);
const providersPromise = useMainStore().getTpaAndSafeliteProviders(zipCode, pageData?.tpaSearchValue ?? '');
const promiseResultMap = [
{
resultKey: 'cmsContent',
promise: cmsContentPromise
},
{
resultKey: 'providers',
promise: providersPromise
}
];
const resultMap = await settleAllPromises(promiseResultMap);
next(async (vm) => { next(async (vm) => {
vm.setCmsContent(cmsContent); vm.setCmsContent(resultMap.cmsContent);
vm.setInitialSearchData(zipCode, filter, providers, providerNumber); vm.mapZipCode = zipCode;
vm.providers = resultMap.providers;
if (pageData) {
vm.tpaSearchValue = pageData?.tpaSearchValue ?? vm.mapZipCode;
vm.radiusFilter = pageData?.radiusFilter ?? 25;
}
}); });
}, },
data() { data() {
return { return {
dataLoaded: false, dataLoaded: false,
zipCode: null, pageData: null,
tpaSearchValue: null,
mapZipCode: null, mapZipCode: null,
filter: '', radiusFilter: null,
providers: [], providers: [],
selectedProviderNumber: '', selectedProviderNumber: useMainStore().order.serviceLocation.provider.providerNumber,
reloadingProviders: false, reloadingProviders: false,
additionalButtonData: { additionalButtonData: {
displayAvailabilityIndicators: false displayAvailabilityIndicators: false
@ -228,28 +212,28 @@ export default {
filterByQuestion: 'FilterByQuestion', filterByQuestion: 'FilterByQuestion',
noNetworkShopsAlert: 'NoNetworkShopsAlertWidget', noNetworkShopsAlert: 'NoNetworkShopsAlertWidget',
shopNotListedLink: 'ShopNotListedLink', shopNotListedLink: 'ShopNotListedLink',
siteFooter: 'SiteFooterWidget' siteFooter: 'SiteFooterWidget',
needHelpLink: 'NeedHelpLink'
}, },
rules: { rules: {
zipCode: `${globalRules.ZIP_CODE_REQUIRED}|${globalRules.ZIP_CODE_SEARCH_FORMAT}`, tpaSearch: globalRules.TPA_SEARCH_FORMAT,
filter: globalRules.OPTION_REQUIRED, // TODO do we even need this? filter: globalRules.OPTION_REQUIRED, // TODO do we even need this?
provider: globalRules.OPTION_REQUIRED provider: globalRules.OPTION_REQUIRED
}, },
shopListButton: shallowRef(shopListButton) tpaShopListButton: shallowRef(tpaShopListButton)
}; };
}, },
computed: { computed: {
filterOptions() { radiusFilterOptions() {
const filterByAnswers = const radiusFilterOptions = this.getCmsContent(
this.getCmsContent(
this.widget.filterByQuestion, this.widget.filterByQuestion,
widgetFields.INPUT_QUESTION_WIDGET.ANSWERS widgetFields.INPUT_QUESTION_WIDGET.ANSWERS
) ?? []; ) ?? [];
const filterByAnswersObj = {}; const options = {};
[...filterByAnswers].forEach((answer) => { for (const option of radiusFilterOptions) {
filterByAnswersObj[answer.Name] = answer.Name; options[option.Name] = option.Text;
}); }
return filterByAnswersObj; return options;
}, },
tpaSearchQuestionLabel() { tpaSearchQuestionLabel() {
return this.getCmsContent( return this.getCmsContent(
@ -257,6 +241,9 @@ export default {
widgetFields.INPUT_QUESTION_WIDGET.QUESTION_TEXT widgetFields.INPUT_QUESTION_WIDGET.QUESTION_TEXT
); );
}, },
needHelpLinkText() {
return this.getCmsContent(this.widget.needHelpLink, widgetFields.TEXT_BLOCK_WIDGET.TEXT);
},
searchInstructionsText() { searchInstructionsText() {
return this.getCmsContent( return this.getCmsContent(
this.widget.searchInstructions, this.widget.searchInstructions,
@ -269,25 +256,25 @@ export default {
widgetFields.TEXT_BLOCK_WIDGET.TEXT widgetFields.TEXT_BLOCK_WIDGET.TEXT
); );
}, },
radiusInMiles() {
return convertRadiusFilterToInteger(this.filter);
},
noNetworkShopsAlertHeaderText() { noNetworkShopsAlertHeaderText() {
return this.getCmsContent( return this.getCmsContent(
this.widget.noNetworkShopsAlert, this.widget.noNetworkShopsAlert,
widgetFields.ALERT_WIDGET.HEADLINE_TEXT widgetFields.ALERT_WIDGET.HEADLINE_TEXT
)?.replaceAll('{custom:radiusInMiles}', this.radiusInMiles); )?.replaceAll('{custom:radiusInMiles}', this.radiusFilter + " miles");
},
radiusFilteredAndCappedProviders() {
return this.providers.filter((provider) => provider.distanceInMiles <= this.radiusFilter).slice(0, 5);
}, },
providerAddresses() { providerAddresses() {
return this.providers?.map((provider) => ({ return this.radiusFilteredAndCappedProviders?.map((provider) => ({
title: provider.companyName, title: provider.companyName.toLowerCase(),
fullAddress: this.getFullProviderAddress(provider), fullAddress: this.getFullProviderAddress(provider),
addressLines: [this.getProviderAddress(provider), this.getProviderCityZipState(provider)] addressLines: [this.getProviderAddress(provider), this.getProviderCityZipState(provider)]
})) ?? []; })) ?? [];
}, },
providerButtonData() { providerButtonData() {
return ( return (
this.providers?.map((provider) => this.radiusFilteredAndCappedProviders?.map((provider) =>
this.getShopButtonDataFromProvider(provider)) ?? [] this.getShopButtonDataFromProvider(provider)) ?? []
); );
}, },
@ -298,14 +285,6 @@ export default {
} }
}, },
watch: { watch: {
async filter() {
if (this.dataLoaded) {
await this.getProviderButtonData().then((providers) => {
this.providers = providers;
});
this.mapZipCode = this.zipCode;
}
},
providers(newProviders) { providers(newProviders) {
if (this.dataLoaded) { if (this.dataLoaded) {
this.selectedProviderNumber = this.selectedProviderNumber =
@ -318,7 +297,6 @@ export default {
const provider = this.providers?.find((p) => p.providerNumber === newNumber); const provider = this.providers?.find((p) => p.providerNumber === newNumber);
if (provider && this.dataLoaded) { if (provider && this.dataLoaded) {
useMainStore().updateServiceLocation({ useMainStore().updateServiceLocation({
searchFilter: this.filter,
zipCode: this.zipCode, zipCode: this.zipCode,
provider: { provider: {
providerNumber: provider?.providerNumber, providerNumber: provider?.providerNumber,
@ -342,13 +320,6 @@ export default {
} }
}, },
methods: { methods: {
setInitialSearchData(zipCode, filter, providers, providerNumber) {
this.zipCode = zipCode;
this.mapZipCode = zipCode;
this.filter = filter;
this.providers = providers;
this.selectedProviderNumber = providerNumber;
},
getFullProviderAddress(provider) { getFullProviderAddress(provider) {
const addressLine1 = this.getProviderAddress(provider); const addressLine1 = this.getProviderAddress(provider);
const addressLine2 = this.getProviderCityZipState(provider); const addressLine2 = this.getProviderCityZipState(provider);
@ -379,16 +350,16 @@ export default {
} }
return addressLine2; return addressLine2;
}, },
async getProviderButtonData() { async getProviderButtonData(shopName = "") {
this.reloadingProviders = true; this.reloadingProviders = true;
const getTpaProvidersResult = await useMainStore().getTpaProviders( const getTpaProvidersResult = await useMainStore().getTpaAndSafeliteProviders(
this.zipCode, this.mapZipCode,
this.radiusInMiles shopName
); );
this.reloadingProviders = false; this.reloadingProviders = false;
return getTpaProvidersResult?.data?.shopProviders ?? []; return getTpaProvidersResult?.data ?? [];
}, },
doNotSeeMyShopLinkClick() { needHelpLinkClick() {
this.mainStore.setBailout(bailoutMessage.RequestCallback()); this.mainStore.setBailout(bailoutMessage.RequestCallback());
this.$router.navigate( this.$router.navigate(
this.navigationScenarios.CLICKED_NEED_HELP, this.navigationScenarios.CLICKED_NEED_HELP,
@ -396,8 +367,12 @@ export default {
); );
}, },
async searchClick() { async searchClick() {
this.providers = await this.getProviderButtonData(); if (isNaN(this.tpaSearchValue)) {
this.mapZipCode = this.zipCode; this.providers = await this.getProviderButtonData(this.tpaSearchValue);
} else {
this.mapZipCode = this.tpaSearchValue;
this.providers = await this.getProviderButtonData();
}
}, },
forwardButtonAction() { forwardButtonAction() {
if (this.selectedProviderIsSafeliteShop) { if (this.selectedProviderIsSafeliteShop) {
@ -407,16 +382,12 @@ export default {
? this.navigationScenarios.CLICKED_FORWARD_WITH_SAFELITE_SHOP ? this.navigationScenarios.CLICKED_FORWARD_WITH_SAFELITE_SHOP
: this.navigationScenarios : this.navigationScenarios
.CLICKED_FORWARD_WITH_NON_SAFELITE_SHOP; .CLICKED_FORWARD_WITH_NON_SAFELITE_SHOP;
// Do not save the zip code as the tpa search value, prevents an outdated zipcode from being used
// as the default search value if customer goes back and changes zip earlier in the flow
const tpaSearchValueToSave = this.tpaSearchValue === this.mapZipCode ? '' : this.tpaSearchValue;
this.savePageDataToStore(issPageValues.TPA_SEARCH, {tpaSearchValue: tpaSearchValueToSave, radiusFilter: this.radiusFilter});
this.$router.navigate(scenario, this.$route); this.$router.navigate(scenario, this.$route);
}, },
getCustomValueFromString(str) {
switch (str) {
case 'radiusInMiles':
return this.radiusInMiles;
default:
return null;
}
},
getShopButtonDataFromProvider(provider) { getShopButtonDataFromProvider(provider) {
const cellNumber = toDisplayPhoneNumber(provider?.phoneNumber); const cellNumber = toDisplayPhoneNumber(provider?.phoneNumber);
const distance = const distance =
@ -426,7 +397,7 @@ export default {
: null; : null;
return { return {
buttonLabel: provider?.companyName ?? '', buttonLabel: provider?.companyName.toLowerCase() ?? '',
buttonLabelSubCopy: distance === null ? '' : `${distance} mi`, buttonLabelSubCopy: distance === null ? '' : `${distance} mi`,
buttonBodyCopy: `${this.getFullProviderAddress(provider)}<br>${ buttonBodyCopy: `${this.getFullProviderAddress(provider)}<br>${
cellNumber ?? '' cellNumber ?? ''
@ -442,4 +413,30 @@ export default {
.darker-gray { .darker-gray {
color: map-get($colors, 'darker-gray'); color: map-get($colors, 'darker-gray');
} }
#tpaSearchQuestionLabel {
margin-bottom: .625rem;
}
#map {
height: 27.1875rem;
}
#providerSelectionForm {
#alertNoNetworkProviders {
:deep(.alert) {
margin-bottom: 0px;
}
}
}
hr {
margin: 1.375rem 0;
}
.need-help-link-container {
margin-top:1.875rem;
margin-bottom: 2.1875rem;
}
</style> </style>

View file

@ -41,13 +41,14 @@ function setupMocks({ glassNameProp, glassLocationProp, colorAnswersProp, modelV
}; };
const wrapper = shallowMount(glassPartQuestion, mountOptions);
// Mock store // Mock store
const partsOrQuestions = pageData ?? { partsOrQuestions: [{ glassName: 'Stationary', glassLocation: 'Rear', parts: [] }] }; const partsOrQuestions = pageData ?? { partsOrQuestions: [{ glassName: 'Stationary', glassLocation: 'Rear', parts: [] }] };
useMainStore().pageData = jest.fn(); useMainStore().pageData = jest.fn();
useMainStore().pageData.mockReturnValue(partsOrQuestions); useMainStore().pageData.mockReturnValue(partsOrQuestions);
document.querySelector = jest.fn().mockReturnValue({ clicked: false, click: jest.fn() }); document.querySelector = jest.fn().mockReturnValue({ clicked: false, click: jest.fn() });
const wrapper = shallowMount(glassPartQuestion, mountOptions);
return { wrapper }; return { wrapper };
} }
@ -62,36 +63,23 @@ describe('glass-part-question.vue', () => {
window.console.log(wrapper.vm.featureListData['Green Tint'][0].Text); window.console.log(wrapper.vm.featureListData['Green Tint'][0].Text);
// Assert // Assert
expect(Object.keys(wrapper.vm.featureListData).length).toBe(2); expect(Object.keys(wrapper.vm.featureListData).length).toBe(2);
expect(wrapper.vm.featureListData['Green Tint'][0].Text).toBe('heated glass, solar, 1 hole'); expect(wrapper.vm.featureListData['Green Tint'][0].Text).toBe('Heated glass, Solar, 1 Hole');
expect(wrapper.vm.featureListData['Green Tint'][0].Name).toBe('DB12209GTYN'); expect(wrapper.vm.featureListData['Green Tint'][0].Name).toBe('DB12209GTYN');
expect(wrapper.vm.featureListData['Gray Tint Privacy'][0].Text).toBe('heated glass, solar, 1 hole'); expect(wrapper.vm.featureListData['Gray Tint Privacy'][0].Text).toBe('Heated glass, Solar, 1 Hole');
expect(wrapper.vm.featureListData['Gray Tint Privacy'][0].Name).toBe('DB12209YPYN'); expect(wrapper.vm.featureListData['Gray Tint Privacy'][0].Name).toBe('DB12209YPYN');
}); });
test('Tint mapper, should get tint image by glassLocation and tintColor', async () => { test('Tint mapper, should get return css class based on tint color', async () => {
// Arrange // Arrange
const { wrapper } = setupMocks(featureListData); const { wrapper } = setupMocks(featureListData);
// Act // Act
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
const tintSourceImage = wrapper.vm.getTintSourceImage('Rear', 'Green Tint'); const tintClass = wrapper.vm.convertTintToCSSClass('Green Tint');
// Assert // Assert
expect(tintSourceImage).toBe('Glass-NoShade-GreenTint.svg'); expect(tintClass).toBe('green-tint');
});
test('Tint mapper, should return empty string if no tint map found', async () => {
// Arrange
const { wrapper } = setupMocks(featureListData);
// Act
await wrapper.vm.$nextTick();
const tintSourceImage = wrapper.vm.getTintSourceImage('Rear', 'Crazy Rainbow Tint');
// Assert
expect(tintSourceImage).toBe('');
}); });
test('Should emit updateModelValue, and have correct attributes', async () => { test('Should emit updateModelValue, and have correct attributes', async () => {
@ -122,7 +110,7 @@ describe('glass-part-question.vue', () => {
{ partNumber: 'DB12209GTYN', color: 'Green Tint' } { partNumber: 'DB12209GTYN', color: 'Green Tint' }
]); ]);
expect(listCard.attributes('groupname')).toBe('Rear-Stationary'); expect(listCard.attributes('groupname')).toBe('Rear-Stationary');
expect(listCard.attributes('validationrules')).toBe('Rear-Stationary-tint-required'); expect(listCard.attributes('validationrules')).toBe('Rear-Stationary-part-required');
}); });
test('default is selected if only one option', async () => { test('default is selected if only one option', async () => {
@ -136,58 +124,42 @@ describe('glass-part-question.vue', () => {
} }
] ]
}; };
const updateSpy = jest.spyOn(glassPartQuestion.methods, 'updateSelectedPartNumber');
const { wrapper } = setupMocks(featureListData); const { wrapper } = setupMocks(featureListData);
// Act
await wrapper.vm.$nextTick();
await wrapper.setData({ selectedTint: 'Green Tint' });
// take emitted value, pass down as modelValue
// yes, yes, it's not ideal
await wrapper.setProps({ modelValue: wrapper.emitted()['update:modelValue'][0][0] });
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
// Assert // Assert
expect(wrapper.vm.selectedPartNumber).toBe('DB12209GTYN'); expect(updateSpy).toHaveBeenCalledWith('DB12209GTYN');
}); });
const partsForSelectedTintTestCases = [ const partsForLocationTestCases = [
[ [
'Rear', 'Rear',
'Stationary', 'Stationary',
'Green Tint',
[ [
{ partNumber: 'Glass1', color: 'Green Tint' }, { partNumber: 'Glass1', color: 'Green Tint' },
{ partNumber: 'Glass2', color: 'Blue Tint' },
{ partNumber: 'Glass3', color: 'Green Tint' }, { partNumber: 'Glass3', color: 'Green Tint' },
{ partNumber: 'Glass4', color: 'Green Tint' }, { partNumber: 'Glass4', color: 'Green Tint' },
{ partNumber: 'Glass6', color: 'Green Tint' } { partNumber: 'Glass5', color: 'Blue Tint' },
{ partNumber: 'Glass6', color: 'Green Tint' },
{ partNumber: 'Glass7', color: 'Red Tint' }
] ]
], ],
[
'Rear',
'Stationary',
'Blue Tint',
[
{ partNumber: 'Glass2', color: 'Blue Tint' },
{ partNumber: 'Glass5', color: 'Blue Tint' }
]
],
['Rear', 'Stationary', 'Red Tint', [{ partNumber: 'Glass7', color: 'Red Tint' }]],
[ [
'Windshield', 'Windshield',
'Single', 'Single',
'Green Tint',
[ [
{ partNumber: 'Windshield1', color: 'Green Tint' }, { partNumber: 'Windshield1', color: 'Green Tint' },
{ partNumber: 'Windshield2', color: 'Green Tint' } { partNumber: 'Windshield2', color: 'Green Tint' }
] ]
], ],
['Windshield', 'Single', 'Blue Tint', []], ['Driver', 'Quarter', []]
['Driver', 'Quarter', 'Green Tint', []]
]; ];
test.each(partsForSelectedTintTestCases)( test.each(partsForLocationTestCases)(
'partsForSelectedTint returns correct parts', 'partsForLocation returns correct parts',
async (glassLocation, glassName, selectedTint, expectedResults) => { async (glassLocation, glassName, expectedResults) => {
// Arrange // Arrange
const pageData = { partsOrQuestions: [ const pageData = { partsOrQuestions: [
{ {
@ -219,11 +191,8 @@ describe('glass-part-question.vue', () => {
pageData pageData
}); });
// Act
await wrapper.setData({ selectedTint });
// Assert // Assert
expect(expectedResults).toEqual(wrapper.vm.partsForSelectedTint); expect(wrapper.vm.partsForLocation).toEqual(expectedResults);
} }
); );
}); });

View file

@ -1,44 +1,27 @@
<template> <template>
<div class="row"> <div class="row">
<p class="mb-0 color-question-text"> <span class="glass-location-text">
{{ colorQuestionText }} {{ partType }}
</p> </span>
</div> </div>
<div class="nested-radio"> <template
<div class="row my-2"> v-for="(tint, index) in tintSelectionOptions"
<div class="col"> :key="index">
<buttonQuestion <div class="tint-option">
v-model="selectedTint" <span>{{ `${partType} - ${tint}` }}</span>
:answers="tintSelectionOptions" <span :class="`tint-image ${convertTintToCSSClass(tint)}`"></span>
buttonTypeString="listCard"
:isWide="true"
altText=""
isRequired
:groupName="replaceAllSpaceWithDash(`${glassLocation}-${glassName}`)"
:validationRules="tintValidationRules">
<div
class="row my-2"
aria-live="polite">
<div class="col">
<buttonQuestion
id="glass-part-question"
v-model="selectedPartNumber"
buttonTypeString="radio"
class="radioQuestion"
:questionText="glassFeatureQuestion"
:answers="featureListData[selectedTint]"
textPosition="text-start"
:loaderEnabled="false"
isRequired
isSmallQuestionText
:groupName="replaceAllSpaceWithDash(`${glassLocation}-${glassName}-${selectedTint}`)"
:validationRules="partValidationRules" />
</div>
</div>
</buttonQuestion>
</div>
</div> </div>
</div> <buttonQuestion
id="glass-part-question"
v-model="selectedPartNumber"
buttonTypeString="list-button"
class="radioQuestion"
:answers="featureListData[tint]"
:loaderEnabled="false"
isRequired
:groupName="replaceAllSpaceWithDash(`${glassLocation}-${glassName}`)"
:validationRules="partValidationRules" />
</template>
</template> </template>
<script> <script>
@ -46,12 +29,10 @@
import buttonQuestion from '@/digital-components/button-question/button-question.vue'; import buttonQuestion from '@/digital-components/button-question/button-question.vue';
// Supporting files // Supporting files
import getTintImage from '@/constants/tint-mapper';
import getCustomTransformValue from '@/constants/dynamictext-mapper';
import { defineRule } from 'vee-validate'; import { defineRule } from 'vee-validate';
import { required } from '@/helpers/validation-rules'; import { required } from '@/helpers/validation-rules';
import errorMessages from '@/constants/error-messages'; import errorMessages from '@/constants/error-messages';
import { useMainStore } from '@/store'; import { toTitleCase } from '@/helpers/text-helper';
export default { export default {
name: 'glass-part-question', name: 'glass-part-question',
@ -72,42 +53,21 @@ export default {
emits: ['update:modelValue'], emits: ['update:modelValue'],
data() { data() {
return { return {
glassColorQuestion: '',
glassFeatureQuestion: '',
selectedTint: ''
}; };
}, },
computed: { computed: {
tintValidationRules() {
const validationRuleName = `${this.glassLocation}-${this.glassName}-tint-required`;
defineRule(validationRuleName, required(errorMessages.OPTION_REQUIRED));
return validationRuleName;
},
partValidationRules() { partValidationRules() {
const validationRuleName = `${this.glassLocation}-${this.glassName}-part-required`; const validationRuleName = `${this.glassLocation}-${this.glassName}-part-required`;
defineRule(validationRuleName, required(errorMessages.OPTION_REQUIRED)); defineRule(validationRuleName, required(errorMessages.OPTION_REQUIRED));
return validationRuleName; return validationRuleName;
}, },
colorQuestionText() {
return getCustomTransformValue(
this.glassColorQuestion,
`${this.glassLocation} ${this.glassName}`
);
},
tintSelectionOptions() { tintSelectionOptions() {
const tintOptions = []; const tintOptions = [];
Object.keys(this.featureListData).forEach((tintOption) => { Object.keys(this.featureListData).forEach((tintOption) => {
const buttonImage = this.getTintSourceImage(this.glassLocation, tintOption); tintOptions.push(tintOption);
tintOptions.push({
value: tintOption,
buttonLabel: tintOption,
buttonImage: buttonImage ? require(`@/assets/img/tints/${buttonImage}`) : null
});
}); });
return tintOptions; return tintOptions;
@ -115,21 +75,21 @@ export default {
selectedPartNumber: { selectedPartNumber: {
get() { get() {
return this.modelValue?.partNumber; return this.modelValue?.partNumber ?? '';
}, },
set(newValue) { set(newValue) {
const part = this.partsForSelectedTint const part = this.partsForLocation
.filter((partItem) => partItem.partNumber === newValue?.value)[0]; .filter((partItem) => partItem.partNumber === newValue)[0];
this.$emit('update:modelValue', part); this.$emit('update:modelValue', part);
} }
}, },
partsForSelectedTint() { partsForLocation() {
const matchingGlass = this.PartDataFromApi.partsOrQuestions?.filter((dataForGlassLocationAndName) => const matchingGlass = this.PartDataFromApi.partsOrQuestions?.filter((dataForGlassLocationAndName) =>
dataForGlassLocationAndName.glassName === this.glassName dataForGlassLocationAndName.glassName === this.glassName
&& dataForGlassLocationAndName.glassLocation === this.glassLocation); && dataForGlassLocationAndName.glassLocation === this.glassLocation);
const matchingGlassParts = matchingGlass?.length === 1 ? matchingGlass[0].parts : []; const matchingGlassParts = matchingGlass?.length === 1 ? matchingGlass[0].parts : [];
return matchingGlassParts.filter((part) => part.color === this.selectedTint) ?? []; return matchingGlassParts;
}, },
// Creates a map of the feature list data in the correct Name/Value // Creates a map of the feature list data in the correct Name/Value
@ -142,7 +102,7 @@ export default {
// Map the data to Name/Text object for ButtonQuestion // Map the data to Name/Text object for ButtonQuestion
const mappedItem = item.FeatureAnswers.reduce((featureArr, it) => { const mappedItem = item.FeatureAnswers.reduce((featureArr, it) => {
featureArr.Text = it.FeatureAnswerText; // Display to User featureArr.Text = this.toTitleCaseWithExceptions(it.FeatureAnswerText); // Display to User
featureArr.Name = it.PartNumber; // Backing Value featureArr.Name = it.PartNumber; // Backing Value
return featureArr; return featureArr;
@ -158,72 +118,19 @@ export default {
PartDataFromApi() { PartDataFromApi() {
return this.mainStore.pageData(this.$route.query.issPage) ?? {}; return this.mainStore.pageData(this.$route.query.issPage) ?? {};
} },
},
watch: { partType() {
selectedTint() { const type = this.partsForLocation?.[0]?.partType ?? '';
this.AutoSelectIfSinglePart(); return toTitleCase(type);
} }
}, },
mounted() { mounted() {
this.LoadPreselectedValues(); this.AutoSelectIfSinglePart();
}, },
methods: { methods: {
// Initialize the component data
initializeComponent(cmsContent) {
this.glassColorQuestion = cmsContent.ColorQuestionWidget.QuestionText;
this.glassFeatureQuestion = cmsContent.FeatureQuestionWidget.QuestionText;
},
// Gets tint images based on the glass type, and tint name.
// Returns an empty string if the src or object is undefined.
getTintSourceImage(glassLocation, tintColor) {
const tintSourceObject = getTintImage(glassLocation, tintColor);
if (tintSourceObject === undefined || !tintSourceObject.src) {
return '';
}
return tintSourceObject.src;
},
AutoSelectIfSinglePart() {
if (this.partsForSelectedTint?.length > 0) {
// Check if only a single part is present for the tint and set the v-model if it is.
if (this.partsForSelectedTint?.length === 1) {
// select element with matching partNumber
this.updateSelectedPartNumber(this.partsForSelectedTint[0].partNumber);
} else {
this.selectedPartNumber = null;
}
}
},
// Loads the preselected values from the store.
LoadPreselectedValues() {
this.$nextTick(() => {
// Populate button-question model-value if parts data already exists in store
if (this.modelValue !== undefined) {
this.selectedTint = this.modelValue.color;
const { glassParts } = useMainStore().order.lineItems;
if (glassParts) {
for (const tintPart of this.partsForSelectedTint) {
for (const glassPart of glassParts) {
if (tintPart.partNumber === glassPart.partNumber) {
this.$nextTick(() => {
this.updateSelectedPartNumber(tintPart.partNumber);
});
return;
}
}
}
}
}
});
},
updateSelectedPartNumber(partNumber) { updateSelectedPartNumber(partNumber) {
this.selectedPartNumber = { value: partNumber }; this.selectedPartNumber = partNumber;
this.$nextTick(() => { this.$nextTick(() => {
const radioInput = document.querySelector(`input[value=${this.selectedPartNumber}]`); const radioInput = document.querySelector(`input[value=${this.selectedPartNumber}]`);
// Fire a click event on the input so the field is updated // Fire a click event on the input so the field is updated
@ -231,37 +138,128 @@ export default {
}); });
}, },
AutoSelectIfSinglePart() {
if (this.partsForLocation?.length > 0) {
// Check if only a single part is present for the tint and set the v-model if it is.
if (this.partsForLocation?.length === 1) {
// select element with matching partNumber
this.updateSelectedPartNumber(this.partsForLocation[0].partNumber);
}
}
},
replaceAllSpaceWithDash(str) { replaceAllSpaceWithDash(str) {
return String(str).replaceAll(' ', '-'); return String(str).replaceAll(' ', '-');
},
convertTintToCSSClass(tint) {
return tint.toLowerCase().replaceAll(' ', '-').replaceAll(',', '');
},
toTitleCaseWithExceptions(text) {
const exceptions = ['side', 'glass', 'dimming'];
let temp = toTitleCase(text);
exceptions.forEach((exception) => {
const regEx = new RegExp(exception, 'ig');
temp = temp.replace(regEx, exception);
});
return temp;
} }
} }
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.nested-radio { .glass-location-text {
:deep(.ui-radio) {
flex-direction: column;
margin: 0.25rem 0;
}
p {
font-size: 0.875rem;
}
}
.color-question-text {
color: $black;
font-weight: $font-weight-bold; font-weight: $font-weight-bold;
color: $black;
} }
.tint-option {
display: flex;
flex-direction: row;
justify-content: space-between;
height: 2rem;
margin-top: .5rem;
margin-bottom: .25rem;
font-weight: 600;
color: $black;
#glass-part-question { @include media-breakpoint-up(md) {
span.fw-bold.w-100 { justify-content: flex-start;
margin-top: 0.5rem;
margin-bottom: 0;
} }
div.col.radio-button-container { }
padding-bottom: 0; .radioQuestion {
:deep(.list-button) {
margin-bottom: 1rem;
} }
} }
.tint-image {
width: 2rem;
height: 2rem;
margin-left: 1rem;
margin-top: -.25rem;
background-repeat: no-repeat;
background-image: url(~@/assets/img/tints/GlassTint.png);
}
.blue-tint-green-shade {
background-position: -53px -53px;
}
.blue-tint-blue-shade {
background-position: -160px -53px;
}
.gray-tint-gray-shade {
background-position: -267px -53px;
}
.blue-tint {
background-position: -373px -53px;
}
.gray-tint {
background-position: -480px -53px;
}
.green-tint-green-shade {
background-position: -53px -160px;
}
.green-tint-blue-shade {
background-position: -160px -160px;
}
.green-tint-gray-shade {
background-position: -267px -160px;
}
.green-tint {
background-position: -373px -160px;
}
.clear {
background-position: -480px -160px;
}
.bronze-tint-green-shade {
background-position: -53px -266px;
}
.bronze-tint-blue-shade {
background-position: -160px -266px;
}
.bronze-tint-gray-shade {
background-position: -267px -266px;
}
.bronze-tint-bronze-shade {
background-position: -373px -266px;
}
.bronze-tint {
background-position: -480px -266px;
}
.bronze-tint-privacy {
background-position: -587px -266px;
}
.clear-blue-shade {
background-position: -53px -373px;
}
.gray-tint-blue-shade {
background-position: -160px -373px;
}
.blue-tint-gray-shade {
background-position: -267px -373px;
}
.gray-tint-privacy {
background-position: -373px -373px;
}
.tinted {
background-position: -480px -373px;
}
</style> </style>

View file

@ -11,34 +11,36 @@
<div class="vehicle-parts-container iss-heritage-content-container-width"> <div class="vehicle-parts-container iss-heritage-content-container-width">
<siteSubHeader <siteSubHeader
ref="siteSubHeader" ref="siteSubHeader"
class="mt-4" class="mt-4 subheader"
cmsWidgetName="SiteSubHeaderWidget" /> cmsWidgetName="SiteSubHeaderWidget" />
<alert <div class="body-container">
id="vehicle-parts-alert" <div
class="mt-3 mb-3" v-for="(item, i) in PartsOrQuestions"
alertClass="alert-warning" :key="i"
cmsWidgetName="AlertWidget" class="glass-question-container">
:isDismissible="false" /> <glassPartQuestion
<div :ref="`${RefPrefix}-${item.glassLocation}-${item.glassName}`"
v-for="(item, i) in PartsOrQuestions" v-model="selectedGlassParts[item.glassLocation + '-' + item.glassName]"
:key="i"> :glassLocation="item.glassLocation"
<!-- Render horizontal lines if there is multi-glass (aka if i > 0) --> :glassName="item.glassName"
<hr v-if="i > 0" /> :colorAnswers="item.colorAnswers"
<glassPartQuestion :alreadyPopulatedPartsData="alreadyPopulatedPartsData" />
:ref="`${RefPrefix}-${item.glassLocation}-${item.glassName}`" </div>
v-model="selectedGlassParts[item.glassLocation + '-' + item.glassName]"
:glassLocation="item.glassLocation"
:glassName="item.glassName"
:colorAnswers="item.colorAnswers"
:alreadyPopulatedPartsData="alreadyPopulatedPartsData" />
</div> </div>
<siteFooter <siteFooter
ref="siteFooter" ref="siteFooter"
class="mt-5"
cmsWidgetName="SiteFooterWidget" cmsWidgetName="SiteFooterWidget"
:isForwardActionDisabled="isForwardActionDisabled" :isForwardActionDisabled="isForwardActionDisabled"
@backClicked="navigateBackByVehicleQuestions" @backClicked="navigateBackByVehicleQuestions"
@ForwardClicked="forwardButtonAction" /> @ForwardClicked="forwardButtonAction" />
<div class="need-help-link-container">
<textLink
v-if="needHelpLinkText"
linkType="navigation"
:text="needHelpLinkText"
href="#"
@clickEvent="requestCallbackBailout" />
</div>
</div> </div>
</div> </div>
</div> </div>
@ -51,7 +53,7 @@ import glassPartQuestion from '@/layouts/vehicle-parts/glass-part-question/glass
import siteHeader from '@/iss-components/site-header/site-header.vue'; import siteHeader from '@/iss-components/site-header/site-header.vue';
import siteSubHeader from '@/iss-components/site-sub-header/site-sub-header.vue'; import siteSubHeader from '@/iss-components/site-sub-header/site-sub-header.vue';
import siteFooter from '@/iss-components/site-footer/site-footer.vue'; import siteFooter from '@/iss-components/site-footer/site-footer.vue';
import alert from '@/ux-components/alert/alert.vue'; import textLink from '@/ux-components/text-link/text-link.vue';
// Supporting Files // Supporting Files
import { fetchCmsContentForPage } from '@/helpers/cms-content-helper'; import { fetchCmsContentForPage } from '@/helpers/cms-content-helper';
@ -61,6 +63,7 @@ import { Form } from 'vee-validate';
import vehicleQuestionsMixin from '@/mixins/vehicle-questions-mixin'; import vehicleQuestionsMixin from '@/mixins/vehicle-questions-mixin';
import BaseFormMixin from '@/mixins/base-form-mixin.js'; import BaseFormMixin from '@/mixins/base-form-mixin.js';
import { useMainStore } from '@/store'; import { useMainStore } from '@/store';
import widgetFields from '@/constants/cms-widget-fields';
export default { export default {
name: 'vehicle-parts', name: 'vehicle-parts',
@ -71,7 +74,7 @@ export default {
siteHeader, siteHeader,
siteSubHeader, siteSubHeader,
siteFooter, siteFooter,
alert textLink
}, },
mixins: [BaseFormMixin, vehicleQuestionsMixin], mixins: [BaseFormMixin, vehicleQuestionsMixin],
async beforeRouteEnter(to, from, next) { async beforeRouteEnter(to, from, next) {
@ -88,17 +91,6 @@ export default {
// Call the "next" function to complete the transition to this page. // Call the "next" function to complete the transition to this page.
next((vm) => { next((vm) => {
vm.setCmsContent(resultMap.cmsContent); vm.setCmsContent(resultMap.cmsContent);
// Glass Part Question dynamic component
Object.keys(vm.$refs)
.filter((r) => r.includes(vm.RefPrefix) && vm.$refs[r][0] !== undefined)
.forEach((c) =>
vm.$refs[c][0].initializeComponent({
ColorQuestionWidget:
resultMap.cmsContent.ColorQuestionWidget,
FeatureQuestionWidget:
resultMap.cmsContent.FeatureQuestionWidget
}));
}); });
}, },
data() { data() {
@ -156,6 +148,9 @@ export default {
RefPrefix() { RefPrefix() {
return 'partQuestion'; return 'partQuestion';
},
needHelpLinkText() {
return this.getCmsContent('NeedHelpLinkWidget', widgetFields.TEXT_BLOCK_WIDGET.TEXT);
} }
}, },
mounted() { mounted() {
@ -220,23 +215,47 @@ export default {
}); });
}); });
}); });
},
requestCallbackBailout() {
this.mainStore.setBailout(bailoutMessage.RequestCallback());
this.$router.navigate(navigationScenarios.CLICKED_NEED_HELP_WITH_BAILOUT, this.$route);
} }
} }
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.iss-heritage-container-width { #app .iss-heritage-container-width {
.iss-heritage-content-container-width {
@include media-breakpoint-up(md) {
width: 58.33333333%;
}
}
.vehicle-parts-container { .vehicle-parts-container {
position: relative; position: relative;
min-height: 1px; min-height: 1px;
padding-left: .9375rem; padding-left: .9375rem;
padding-right: .9375rem; padding-right: .9375rem;
#vehicle-parts-alert { .subheader {
.alert-heading { :deep(strong) {
margin-top: 0.25rem !important; text-transform: uppercase;
color: $black;
font-weight: 600;
} }
} }
.glass-question-container {
margin-bottom: 1.25rem;
}
.body-container {
margin-bottom: 1.875rem;
}
.need-help-link-container {
margin-bottom: 2.25rem;
}
} }
} }
</style> </style>

View file

@ -248,6 +248,10 @@ const routingTable = () => [
{ {
scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS, scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS,
destinationIssPageValue: issPageValues.COVERAGE_STATEMENT destinationIssPageValue: issPageValues.COVERAGE_STATEMENT
},
{
scenario: navigationScenarios.CLICKED_NEED_HELP_WITH_BAILOUT,
destinationIssPageValue: issPageValues.BAILOUT_PAGE
} }
] ]
}, },

View file

@ -142,7 +142,7 @@ export const getDefaultState = () => ({
appointmentType: null, appointmentType: null,
isVehicleProtected: null, isVehicleProtected: null,
IsSafeliteProvider: null, IsSafeliteProvider: null,
searchFilter: null, tpaSearchRadius: null,
provider: { provider: {
providerNumber: null, providerNumber: null,
address: { address: {
@ -1038,8 +1038,24 @@ export const useMainStore = defineStore({
return this.getProviders(zipCode, radius, true) return this.getProviders(zipCode, radius, true)
}, },
getTpaProviders(zipCode, radius) { getTpaAndSafeliteProviders(zipCode, provderNameSearchString = "") {
return this.getProviders(zipCode, radius, false) const { parentAccountNumber } = this.order;
const carId = this.order.vehicle.carId;
const damageType = this.damage.isRepair ? 'Repair' : 'Install';
const parts = getLineItemsFlattened(this.order.lineItems.glassParts);
const partNumbers = parts.map((part) => part.partNumber).join(',');
let url = `${endpoints.GetTpaAndSafeliteProviders.url}/${zipCode}?accountNumber=${parentAccountNumber}&carId=${carId}&damageType=${damageType}&partNumbers=${partNumbers}`;
if (provderNameSearchString) {
url += `&providerNameSearchString=${provderNameSearchString}`;
}
return new Promise((resolve, reject) => {
globalMethods.callHttpClient({
method: endpoints.GetTpaAndSafeliteProviders.method,
endpoint: url
}).then((response) => resolve(response), (error) => reject(error));
});
}, },
getProviders(zipcode, radius, safeliteOnly) { getProviders(zipcode, radius, safeliteOnly) {
@ -1844,7 +1860,7 @@ export const useMainStore = defineStore({
phoneNumber: serviceLocationInfo.provider?.phoneNumber phoneNumber: serviceLocationInfo.provider?.phoneNumber
}; };
} }
this.order.serviceLocation.searchFilter = serviceLocationInfo.searchFilter; this.order.serviceLocation.tpaSearchRadius = serviceLocationInfo.tpaSearchRadius;
}, },
resetState() { resetState() {

View file

@ -1966,53 +1966,65 @@ describe('Store', () => {
}); });
}); });
describe('getTpaProviders method', () => { describe('getTpaAndSafeliteProviders method', () => {
describe('successful method call', () => { describe('successful method call', () => {
it.each([[true, 'Repair'], [false, 'Replace']])( it.each([[true, 'Repair'], [false, 'Install']])(
'calls getProviders api endpoint', 'calls getProviders api endpoint with damageType %s',
async (isRepair, damageType) => { async (isRepair, damageType) => {
// Arrange // Arrange
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve({})); globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve({}));
const carId = getRandomString(10, 14); const carId = getRandomString(10, 14);
const parentAccountNumber = getRandomString(6, 6); const parentAccountNumber = getRandomString(6, 6);
const partNumber = getRandomString(6, 6);
store.order.parentAccountNumber = parentAccountNumber; store.order.parentAccountNumber = parentAccountNumber;
store.order.damage.isRepair = isRepair; store.order.damage.isRepair = isRepair;
store.order.vehicle.carId = carId; store.order.vehicle.carId = carId;
const zipCode = getRandomString(6, 6); store.order.lineItems.glassParts = [{ partNumber }];
const radius = getRandomInt(5, 100); const zipCode = getRandomString(5, 5);
const isBigTruck = false;
store.order.vehicle.isBigTruck = isBigTruck; const expectedEndpoint = `${endpoints.GetTpaAndSafeliteProviders.url}/${zipCode}?accountNumber=${parentAccountNumber}&carId=${carId}&damageType=${damageType}&partNumbers=${partNumber}`;
const expectedEndpoint = endpoints.GetProviders.url(
zipCode,
damageType,
radius,
parentAccountNumber,
'false',
carId,
null,
isBigTruck
);
// Act // Act
await store.getTpaProviders(zipCode, radius); await store.getTpaAndSafeliteProviders(zipCode);
// Asserts // Asserts
expect(globalMethods.callHttpClient).toHaveBeenCalledWith(expect.objectContaining({ expect(globalMethods.callHttpClient).toHaveBeenCalledWith(expect.objectContaining({
method: endpoints.GetProviders.method, method: endpoints.GetTpaAndSafeliteProviders.method,
endpoint: expectedEndpoint endpoint: expectedEndpoint
})); }));
} }
); );
it('includes providerNameSearchString in endpoint when provided', async () => {
// Arrange
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve({}));
const carId = getRandomString(10, 14);
const parentAccountNumber = getRandomString(6, 6);
const partNumber = getRandomString(6, 6);
store.order.parentAccountNumber = parentAccountNumber;
store.order.damage.isRepair = true;
store.order.vehicle.carId = carId;
store.order.lineItems.glassParts = [{ partNumber }];
const zipCode = getRandomString(5, 5);
const providerNameSearchString = 'Test Shop';
// Act
await store.getTpaAndSafeliteProviders(zipCode, providerNameSearchString);
// Asserts
expect(globalMethods.callHttpClient).toHaveBeenCalledWith(expect.objectContaining({
endpoint: expect.stringContaining(`&providerNameSearchString=${providerNameSearchString}`)
}));
});
it('Returns expected response object', async () => { it('Returns expected response object', async () => {
// Arrange // Arrange
const response = { ReferralNumber: getRandomString(6, 6) }; const response = { ReferralNumber: getRandomString(6, 6) };
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve(response)); globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve(response));
const zipCode = getRandomString(6, 6); const zipCode = getRandomString(5, 5);
const radius = getRandomInt(5, 100);
// Act // Act
const result = store.getTpaProviders(zipCode, radius); const result = store.getTpaAndSafeliteProviders(zipCode);
// Asserts // Asserts
await expect(result).resolves.toBe(response); await expect(result).resolves.toBe(response);
@ -2020,11 +2032,10 @@ describe('Store', () => {
it('null response by api => no exceptions thrown', async () => { it('null response by api => no exceptions thrown', async () => {
// Arrange // Arrange
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve(null)); globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve(null));
const zipCode = getRandomString(6, 6); const zipCode = getRandomString(5, 5);
const radius = getRandomInt(5, 100);
// Act // Act
await store.getTpaProviders(zipCode, radius); await store.getTpaAndSafeliteProviders(zipCode);
}); });
it('no providers returned by api => promise resolves to empty list', async () => { it('no providers returned by api => promise resolves to empty list', async () => {
// Arrange // Arrange
@ -2034,11 +2045,10 @@ describe('Store', () => {
} }
}; };
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve(responseNoProviders)); globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve(responseNoProviders));
const zipCode = getRandomString(6, 6); const zipCode = getRandomString(5, 5);
const radius = getRandomInt(5, 100);
// Act // Act
const result = await store.getTpaProviders(zipCode, radius); const result = await store.getTpaAndSafeliteProviders(zipCode);
// Asserts // Asserts
expect(result.data.shopProviders).toHaveLength(0); expect(result.data.shopProviders).toHaveLength(0);
@ -2060,7 +2070,7 @@ describe('Store', () => {
globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve(responseProviders)); globalMethods.callHttpClient = jest.fn().mockReturnValue(Promise.resolve(responseProviders));
// Act // Act
const result = await store.getTpaProviders(); const result = await store.getTpaAndSafeliteProviders();
// Asserts // Asserts
expect(result.data.shopProviders).toHaveLength(2); expect(result.data.shopProviders).toHaveLength(2);