Merge pull request #290 from Safelite/feature/digital/SSR-405

Feature/digital/ssr 405 and 406 Provider Preference Redesign
This commit is contained in:
Jason Wheeler 2023-05-08 14:14:39 -04:00 committed by GitHub
commit f4b1ea8170
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 682 additions and 370 deletions

View file

@ -0,0 +1,11 @@
<svg viewBox="0 7 78 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.83691 15.1348L8.94207 14.6427C9.08672 13.9643 8.77363 13.3983 7.85528 13.3983C6.83511 13.3983 6.22273 14.117 6.11233 14.6337C5.62652 16.9121 13.6185 15.7976 12.7192 20.014C12.1953 22.4697 9.69054 23.5686 5.50708 23.5686C1.57914 23.5686 -0.431696 22.7119 0.0779134 20.3212L0.164038 19.9168H4.75576L4.6506 20.4094C4.44028 21.3945 5.00223 21.75 5.92058 21.75C6.88983 21.75 7.60072 21.0484 7.73537 20.4179C8.22119 18.14 0.531851 19.2706 1.40356 15.1832C1.89985 12.8569 4.13671 11.5802 8.14125 11.5802C12.2733 11.5802 13.8026 12.663 13.2754 15.1348H8.83691Z" fill="#DA291C"/>
<path d="M18.8241 19.8334C18.1608 20.0003 17.8315 20.3473 17.7154 20.8906C17.5993 21.4353 17.8843 21.9999 18.5286 21.9999C19.4831 21.9999 19.9618 21.5472 20.1521 20.6555L20.3843 19.5651C20.3843 19.5651 19.4869 19.6665 18.8241 19.8334ZM20.8763 17.2127C20.9829 16.714 20.6893 16.4571 19.9499 16.4571C19.1386 16.4571 18.6623 16.9013 18.4544 17.7627H14.732C15.2321 15.4203 18.3016 15.0121 20.2582 15.0121C23.2645 15.0121 25.2582 15.5108 24.8033 17.6418L23.9521 21.6316C23.7908 22.3873 23.7127 22.977 23.9806 23.3999H19.8286C19.7467 23.1131 19.7639 22.8101 19.7629 22.4778H19.7153C19.0834 23.2036 17.851 23.6123 16.5625 23.6123C13.9854 23.6123 13.196 22.6139 13.4958 21.2082C13.8184 19.6974 14.8048 18.9872 17.636 18.579L19.3851 18.3221C20.2069 18.2054 20.4619 18.186 20.677 18.1481L20.8763 17.2127Z" fill="#DA291C"/>
<path d="M27.0505 17.0521H25.5711L25.9613 15.2236H27.4406L27.7109 13.9554C28.0592 12.3232 29.1902 11.7188 32.1732 11.7188C32.7932 11.7188 33.5507 11.7491 34.2059 11.8093L33.8881 13.2998C33.6797 13.2694 33.4917 13.2543 33.182 13.2543C32.6566 13.2543 32.1204 13.4202 32.0333 13.8289L31.7354 15.2236H33.4774L33.0873 17.0521H31.3453L29.9911 23.3999H25.6958L27.0505 17.0521Z" fill="#DA291C"/>
<path d="M39.793 17.7112C39.9767 16.8498 39.7744 16.4573 38.8675 16.4573C37.9848 16.4573 37.6147 16.8498 37.431 17.7112L37.3068 18.292H39.6693L39.793 17.7112ZM36.9908 19.7734L36.7158 21.0629C36.5288 21.94 37.0532 22.1666 37.6494 22.1666C38.4131 22.1666 38.9774 21.6475 39.2158 20.5286H43.0814C42.6113 22.5084 40.9207 23.612 37.3648 23.612C33.2133 23.612 32.1284 22.4325 32.7926 19.3198C33.4602 16.1909 35.334 15.0118 39.1996 15.0118C42.6356 15.0118 44.1715 15.9794 43.5396 18.9419L43.3621 19.7734H36.9908Z" fill="#DA291C"/>
<path d="M46.6878 11.3427H50.9831L48.4108 23.4002H44.1155L46.6878 11.3427Z" fill="#DA291C"/>
<path d="M52.8394 11.3427H57.1342L56.6098 13.8026H52.3141L52.8394 11.3427ZM52.0114 15.2234H56.3062L54.5628 23.3974H50.268L52.0114 15.2234Z" fill="#DA291C"/>
<path d="M57.2803 15.2234H58.712L58.9338 14.1814L63.9361 10.7557L63.0068 15.2234H64.7013L64.3106 17.0519H62.6167L61.7383 21.1692C61.6384 21.6381 61.9372 21.8036 62.629 21.8036H63.2971L62.9569 23.4002C62.1561 23.46 61.3143 23.4908 60.5992 23.4908C57.974 23.4908 57.0062 23.2187 57.3931 21.4049L58.3214 17.0519H56.8901L57.2803 15.2234Z" fill="#DA291C"/>
<path d="M71.351 17.7112C71.5347 16.8498 71.332 16.4573 70.4255 16.4573C69.5424 16.4573 69.1722 16.8498 68.9885 17.7112L68.8648 18.292H71.2268L71.351 17.7112ZM68.5484 19.7734L68.2734 21.0629C68.0869 21.94 68.6107 22.1666 69.2074 22.1666C69.9711 22.1666 70.535 21.6475 70.7734 20.5286H74.639C74.1693 22.5084 72.4783 23.612 68.9234 23.612C64.7708 23.612 63.6864 22.4325 64.3507 19.3198C65.0183 16.1909 66.8921 15.0118 70.7577 15.0118C74.1941 15.0118 75.7291 15.9794 75.0972 18.9419L74.9197 19.7734H68.5484Z" fill="#DA291C"/>
<path d="M76.2645 23.0422C76.4482 23.0365 76.4905 22.9606 76.4905 22.8648C76.4905 22.7719 76.4482 22.7245 76.293 22.7245H76.058V23.0422H76.2645ZM76.058 23.236V23.7741H75.8634V22.5302H76.2673C76.4957 22.5302 76.7332 22.5923 76.7332 22.8729C76.7332 23.0166 76.6461 23.1299 76.4791 23.1635V23.1687C76.6518 23.2024 76.6713 23.2782 76.6908 23.4138C76.708 23.5314 76.7222 23.6618 76.7646 23.7741H76.5157C76.5019 23.7035 76.4819 23.6248 76.4734 23.5518C76.4591 23.4446 76.4591 23.3465 76.4029 23.2872C76.3544 23.236 76.2902 23.2422 76.2221 23.236H76.058ZM77.1571 23.1664C77.1571 22.6875 76.7674 22.2969 76.284 22.2969C75.7987 22.2969 75.4056 22.6875 75.4056 23.1664C75.4056 23.6504 75.7987 24.041 76.284 24.041C76.7674 24.041 77.1571 23.6504 77.1571 23.1664ZM75.2196 23.1664C75.2196 22.5832 75.694 22.1106 76.284 22.1106C76.8716 22.1106 77.3437 22.5832 77.3437 23.1664C77.3437 23.7542 76.8716 24.2269 76.284 24.2269C75.694 24.2269 75.2196 23.7542 75.2196 23.1664Z" fill="#DA291C"/>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View file

@ -3,7 +3,8 @@ const dynamicStrings = {
CUSTOM: "custom",
ROUTER_LINK: "routerLink:",
MODAL_LINK: "modalLink",
TEXT_LINK: 'textLink'
TEXT_LINK: 'textLink',
EXTERNAL_LINK: 'externalLink',
};
export { dynamicStrings };

View file

@ -67,10 +67,10 @@
</div>
</fieldset>
</div>
<div class="row form-test-error mt-1">
<error-message
<div v-if="!suppressError" class="row form-test-error mt-1">
<error-message
:name="formatString(groupName)"
v-if="!suppressError"></error-message>
></error-message>
</div>
</div>
</template>
@ -81,6 +81,7 @@ import listButtonHorizontal from "@/ux-components/list-button-horizontal/list-bu
import listCard from "@/ux-components/list-card/list-card";
import radio from "@/ux-components/radio/radio";
import { ErrorMessage } from "vee-validate";
import providerPrefRadio from "@/layouts/provider-preference/provider-pref-radio/provider-pref-radio.vue"
export default {
name: "buttonQuestion",
@ -172,6 +173,9 @@ export default {
case "servicePackageRadio":
classes = "package-main";
break;
case "providerPrefRadio":
classes = "option-main";
break;
}
return classes;
},
@ -180,10 +184,17 @@ export default {
classes += this.isWide ? "col-12" : "col";
if (this.buttonTypeString == "radio") {
classes += " radio-button-container";
} else if (this.buttonTypeString == "servicePackageRadio") {
classes = "package-wrapper";
switch(this.buttonTypeString) {
case "radio":
classes += " radio-button-container";
break;
case "servicePackageRadio":
classes = "package-wrapper";
break;
case "providerPrefRadio":
classes = "option-wrapper";
break;
default:
}
return classes;
@ -228,6 +239,7 @@ export default {
listCard,
ErrorMessage,
radio,
providerPrefRadio
},
};
</script>

View file

@ -37,6 +37,12 @@ export default {
},
error => {
console.error(error);
//implement if analytics service is down
if(endpoint.includes("analytics")){
return resolve({data: ""});
}
return reject(error.response);
}
);

View file

@ -145,18 +145,24 @@ function findAndReplaceGlobalStateValues(widgetModel, widgetName) {
function processWidgetItemForReplacement(widgetModel, key) {
// If we have a string, and it needs to be replaced.
if (typeof widgetModel[key] === 'string') {
widgetModel[key] = processIfStatements(
widgetModel[key],
dynamicStrings.GLOBAL_STATE,
getStoreValueFromString
);
if (widgetModel[key].includes(dynamicStrings.GLOBAL_STATE)) {
widgetModel[key] = mapStringToState(widgetModel[key]);
if (widgetModel[key].includes("{if:"))
{
widgetModel[key] = processIfStatements(
widgetModel[key],
dynamicStrings.GLOBAL_STATE,
getStoreValueFromString
);
}
if (widgetModel[key].includes(dynamicStrings.MODAL_LINK)) {
widgetModel[key] = mapStringToModal(widgetModel[key]);
}
if (widgetModel[key].includes(dynamicStrings.EXTERNAL_LINK)) {
widgetModel[key] = mapStringToLink(widgetModel[key]);
}
if (widgetModel[key].includes(dynamicStrings.GLOBAL_STATE)) {
widgetModel[key] = mapStringToState(widgetModel[key]);
}
return widgetModel[key];
}
@ -194,6 +200,24 @@ function mapStringToModal(str) {
return returnVal;
}
function mapStringToLink(str) {
const startIndex = str.indexOf('{' + dynamicStrings.EXTERNAL_LINK);
let linkToReplace = str.substring(startIndex, str.length);
linkToReplace = linkToReplace.substring(0, linkToReplace.indexOf('}') + 1);
const params = linkToReplace.substring((dynamicStrings.EXTERNAL_LINK).length + 2, linkToReplace.length - 1);
const splitParams = params.split(',');
const bodyText = '<a href="' + splitParams[0] + '" class="external-text" target="_blank">' + splitParams[1] + '</a>';
let returnVal = str.replace(linkToReplace, bodyText);
if (returnVal.includes(dynamicStrings.EXTERNAL_LINK)) {
returnVal = mapStringToLink(returnVal);
}
return returnVal;
}
// Function to convert a string, into a matching global state item.
function mapStringToState(str) {
// Pull all matches out of the string.
@ -221,10 +245,10 @@ function mapStringToState(str) {
}
// Concatenate the string.
stringBuilder = `${stringBuilder} ${stringWithReplacement}`;
str = `${stringBuilder} ${stringWithReplacement}`;
}
return stringBuilder.trimStart();
return str.trimStart();
}
function getStoreValueFromString(str) {

View file

@ -14,8 +14,7 @@
</div>
<div class="d-flex align-items-center container-fluid overflow-hidden" :class="justifySubheader">
<p class="text-center fw-normal mb-0 subheader-secondary" :class="alternateFormatting">
<span>
{{ subText }}
<span v-html="subText">
</span>
</p>
</div>

View file

@ -2,7 +2,7 @@ import { mount } from "@vue/test-utils"
import providerPrefRadio from "./provider-pref-radio"
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin"
describe.skip("provider-pref-radio.vue", () => {
describe("provider-pref-radio.vue", () => {
it("Should include buttonLabel in html", async () => {
// Arrange
let { wrapper } = setupMocks({

View file

@ -1,12 +1,12 @@
<template>
<baseInputButton v-bind="$props" @buttonClicked="handleAnswerChange" v-model="selectedValue">
<div class="package-label mb-4"
<baseInputButton v-bind="$props" v-model="selectedValue">
<div class="option-label"
:class="[this.buttonLabelSubCopy ? 'has-subheader' : '']"
for="testradio">
<div class="package-specs">
<div class="option-specs">
<p class="m-0">
<span v-html="this.buttonLabel"></span>
<span class="pricing-info" v-html="this.buttonAuxiliaryCopy"></span>
<span class="option-info" v-html="this.buttonAuxiliaryCopy"></span>
</p>
<p class="sub-label m-0"
v-if="this.buttonLabelSubCopy"
@ -15,34 +15,9 @@
<ul >
<li v-for="listItem in this.arrayOfListItemsFromBodyText" :key="listItem" class="mb-0">
<!-- no textLink -->
<span v-if="!doesCopyContainTextLink(listItem)"
v-html="listItem"></span>
<!-- with textLink -->
<template v-else
v-for="copy in splitCopyOnCMSPlaceHolder(listItem)"
:key="copy">
<span v-if="!doesCopyContainTextLink(copy)" v-html="copy"></span>
<span v-else>
<textLink linkType="text"
:text="getRouterLinkDisplayTextFromCopy(copy)"
href="#!"
@click-event="
$emit('buttonEvent', {
eventName: 'openModal',
args: getRouterLinkRouteFromCopy(copy),
})
"
:data-bs-target="'#' + getRouterLinkRouteFromCopy(copy)"
aria-label="Modal window" />
</span>
</template>
<span v-html="listItem"></span>
</li>
</ul>
<!-- End of body text parsing -->
<!--ms-n6-->
<div class="package-footer fw-bold caption mt-4 ml-n4 mr-3"
v-if="this.buttonFooterCopy"
v-html="this.buttonFooterCopy"></div>
</div>
</div>
</div>
@ -53,14 +28,9 @@
import baseInputButton from '@/digital-components/base-input-button/base-input-button';
import textLink from '@/ux-components/text-link/text-link';
import inputButtonWrapperMixin from '@/mixins/input-button-wrapper-mixin';
import {
getRouterLinkDisplayTextFromCopy,
getRouterLinkRouteFromCopy,
splitCopyOnCMSPlaceHolder,
doesCopyContainTextLink,
} from '@/helpers/cms-content-helper';
export default {
name: 'servicePackageRadio',
name: 'providerPrefRadio',
mixins: [inputButtonWrapperMixin],
components: {
baseInputButton,
@ -72,13 +42,6 @@ export default {
},
},
methods: {
getRouterLinkDisplayTextFromCopy,
getRouterLinkRouteFromCopy,
splitCopyOnCMSPlaceHolder,
doesCopyContainTextLink,
stripUlTagFromCopy(copy) {
return copy;
},
getArrayOfListItemsFromRawCmsCopy(copy) {
if (copy) {
return copy
@ -91,14 +54,24 @@ export default {
</script>
<style lang="scss" scoped>
.ml-n4 {
margin-left: -$spacer * 2;
.option-wrapper:last-child .option-label{
margin-bottom: .25rem;
}
.mr-3 {
margin-right: $spacer * 1.5;
.option-wrapper .option-label{
margin-bottom: 1rem;
}
.package-main {
.package-wrapper {
.option-main {
.has-error > .option-label {
border: 1px solid $red !important;
&:hover {
box-shadow: 0 0 0 4px $red-200 !important;
z-index: 5 !important;
}
}
.option-wrapper {
margin: 0.5rem 0;
label {
@ -110,7 +83,7 @@ export default {
position: absolute;
left: -9999px;
+ .package-label {
+ .option-label {
display: flex;
align-items: flex-start;
position: relative;
@ -152,7 +125,7 @@ export default {
}
&:hover {
+ .package-label {
+ .option-label {
&:before {
border: 1px solid #8e9292;
box-shadow: 0px 0px 0px 4px #9fcee6, 0px 1px 4px rgba(0, 0, 0, 0.2);
@ -161,33 +134,33 @@ export default {
}
&:checked {
+ .package-label {
.package-specs {
+ .option-label {
.option-specs {
max-height: 1000px;
}
}
+ .package-label {
.package-specs {
+ .option-label {
.option-specs {
.hide-when-closed {
display: block;
}
}
}
+ .package-label {
+ .option-label {
background-color: $blue-100;
border: 1px solid $blue;
max-height: 500px;
}
+ .package-label {
+ .option-label {
&:before {
box-shadow: 0px 0px 0px 1px $blue;
}
}
+ .package-label {
+ .option-label {
&:after {
background: $blue;
}
@ -195,7 +168,7 @@ export default {
}
&:focus {
+ .package-label {
+ .option-label {
&:before {
border: 2px solid $blue;
}
@ -203,11 +176,7 @@ export default {
}
}
.package-footer {
color: $red;
}
.package-specs {
.option-specs {
display: flex;
flex-direction: column;
width: 100%;
@ -220,7 +189,7 @@ export default {
justify-content: space-between;
span {
&.pricing-info {
&.option-info {
color: $green;
font-size: 0.875rem;
}
@ -248,17 +217,7 @@ export default {
}
}
}
@keyframes slideaway {
from {
display: block;
}
to {
transform: translateY(40px);
opacity: 0;
}
}
}
}
}

View file

@ -1,204 +1,119 @@
import { mount } from '@vue/test-utils';
import { navigationScenarios } from '@/router/router-constants/navigation-scenarios';
import { issPageValues } from '@/router/router-constants/issPage-values';
import { queryStrings } from '@/constants/query-strings';
import { GaActions } from "@/constants/analytics";
import ProviderPreference from "@/layouts/provider-preference/provider-preference.vue"
import { useMainStore } from '@/store';
import { createTestingPinia } from '@pinia/testing';
import { mapStores } from "pinia";
const pinia = createTestingPinia();
useMainStore(pinia);
import { shallowMount } from '@vue/test-utils';
import { settleAllPromises } from "@/helpers/layout-helper.js";
import { fetchCmsContentForPage, setupModalLinks } from "@/helpers/cms-content-helper";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { useMainStore } from "@/store";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
import { nextTick } from "vue";
const ProviderPreferenceMockData = {
QuestionText: 'Which auto glass shop would you like to work with?',
Answers: [
{
Name: 'Schedule with Safelite',
Text: 'Schedule with Safelite',
SubText: '',
ImageId: '',
SubWidgetName: '',
},
{
Name: 'Find another shop',
Text: 'Find another shop',
SubText: '',
ImageId: '',
SubWidgetName: '',
},
],
};
const siteFooterWidgetMockData = {
ForwardButtonText: 'Continue',
};
function setupMocks() {
const mockRoute = {
query: {
issPage: 'provider-preference',
},
};
const mockRouter = {
navigate: jest.fn(),
};
const wrapper = mount(ProviderPreference, {
data() {
return {
SelectedshopLocation: null,
};
},
global: {
mixins: [
{
computed: {
...mapStores(useMainStore),
GaActions() {
return GaActions;
},
queryStrings() {
return queryStrings;
},
navigationScenarios() {
return navigationScenarios;
},
},
methods: {
getCmsContent: jest.fn((cmsWidgetName, fieldName) => {
if (cmsWidgetName === 'SiteFooterWidget') {
if (fieldName === 'ForwardButtonText') {
return siteFooterWidgetMockData.ForwardButtonText;
}
}
// Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({
settleAllPromises: jest.fn(),
}));
if (cmsWidgetName === 'StateSteeringModal') {
if (fieldName === 'BodyText') {
return "{if:custom:OH}ohioText{end}{if:custom:CA}cali's Text{end}";
}
}
// Mock fetchCmsContentForPage
jest.mock("@/helpers/cms-content-helper", () => ({
fetchCmsContentForPage: jest.fn(),
setupModalLinks: jest.fn(),
}));
if (cmsWidgetName === 'ProviderPreference') {
if (fieldName === 'QuestionText') {
return ProviderPreferenceMockData.QuestionText;
}
if (fieldName === 'Answers') {
return ProviderPreferenceMockData.Answers;
}
}
return '';
}),
getFooterInfoBoxHeight: jest.fn(() => 80),
getPageNameByQueryString: jest.fn(() => "provider-preference"),
pushEventToGA: jest.fn(),
},
},
],
mocks: {
$route: mockRoute,
$router: mockRouter,
},
stubs: {
SiteHeader: true,
SiteSubHeader: true,
},
},
});
return { mockRoute, mockRouter, wrapper };
}
describe('provider-preference.vue', () => {
test("getStateSpecificText returns true when values match", () => {
const { wrapper } = setupMocks();
useMainStore().order.customer.address.state = "ohio";
const actual = wrapper.vm.getStateSpecificText("Ohio")
expect(actual).toBeTruthy();
});
test("getStateSpecificText returns false when values do not match", () => {
const { wrapper } = setupMocks();
useMainStore().order.customer.address.state = "delaware";
const actual = wrapper.vm.getStateSpecificText("Ohio")
expect(actual).toBeFalsy();
});
test("getStateSpecificText should return correct value for state", () => {
const { wrapper } = setupMocks();
useMainStore().order.customer.address.state = "ca";
let actual = wrapper.vm.steeringModalBody;
expect(actual).toBe("cali's Text");
useMainStore().order.customer.address.state = "oH";
actual = wrapper.vm.steeringModalBody;
expect(actual).toBe("ohioText");
});
test.skip('"Continue" button is disabled when no Shop Location is selected.', () => {
const { wrapper } = setupMocks();
test("Should display steering modal if steering modal data is present", async () => {
// Arrange
const { wrapper } = setupMocks();
const continueButton = wrapper.get('[data-test-id="site-footer-main-button"]');
expect(continueButton.attributes()['aria-disabled']).toBe('true');
});
test.skip('"Continue" button is enabled after a Shop Location is selected.', async () => {
const { wrapper } = setupMocks();
// Act
wrapper.vm.getSteeringModalBody = jest.fn().mockReturnValue("something");
wrapper.vm.setShowSteeringLink();
console.log(wrapper.vm.showSteeringLink);
const ProviderPreferenceWrapper = wrapper.findComponent({ name: 'provider-preference' });
// Not using initializeComponent() because it doesn't trigger reactivity in test.
await ProviderPreferenceWrapper.setData({
questionTextFromCms: ProviderPreferenceMockData.QuestionText,
answersFromCms: ProviderPreferenceMockData.Answers,
SelectedshopLocation:"Schedule with Safelite"
});
// Test
expect(wrapper.vm.showSteeringLink).toBeTruthy();
const continueButton = wrapper.get('[data-test-id="site-footer-main-button"]');
expect(continueButton.attributes()['aria-disabled']).toBe('false');
});
});
test.skip('Select "Schedule with Safelite" then click "Continue". Navigates to "service-location" page.', async () => {
const { mockRoute, mockRouter, wrapper } = setupMocks();
test("Should not display steering modal if steering modal data is not present", async () => {
// Arrange
const { wrapper } = setupMocks();
const ProviderPreferenceWrapper = wrapper.findComponent({ name: 'provider-preference' });
// Not using initializeComponent() because it doesn't trigger reactivity in test.
await ProviderPreferenceWrapper.setData({
questionTextFromCms: ProviderPreferenceMockData.QuestionText,
answersFromCms: ProviderPreferenceMockData.Answers,
SelectedshopLocation:"Schedule with Safelite"
});
// Act
wrapper.vm.getSteeringModalBody = jest.fn().mockReturnValue("");
wrapper.vm.setShowSteeringLink();
console.log(wrapper.vm.showSteeringLink);
// Test
expect(wrapper.vm.showSteeringLink).toBeFalsy();
const continueButton = wrapper.get('[data-test-id="site-footer-main-button"]');
await continueButton.trigger('click');
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
expect(mockRouter.navigate)
.toHaveBeenCalledWith(navigationScenarios.CLICKED_FORWARD_WITH_SAFELITE, mockRoute);
/**
* To Do: find a way to confirm that Vue Router really routes to the expected page.
* Currently just trust that our custom navigate() method does its job properly.
*/
});
test.skip('Click the back button, trigger navigate function from Vue Router with CLICKED_BACK parameter.', async () => {
const { mockRoute, mockRouter, wrapper } = setupMocks();
await wrapper.get('[data-test-id="site-footer-back-button"]').trigger('click');
expect(mockRouter.navigate).toHaveBeenCalledTimes(1);
expect(mockRouter.navigate)
.toHaveBeenCalledWith(navigationScenarios.CLICKED_BACK, mockRoute);
});
});
test("Should navigate to safelite flow when safelite selected", () =>
{
// Arrange
const { wrapper } = setupMocks();
// Act
wrapper.vm.selectedProvider = "SafeliteOption";
wrapper.vm.forwardButtonAction();
// Test
expect(wrapper.vm.$router.navigate).toBeCalledWith(navigationScenarios.CLICKED_FORWARD_WITH_SAFELITE, "provider-preference");
});
test("Should navigate to tpa flow when TPAOption selected and TPA Flow Enabled", () =>
{
// Arrange
const { wrapper } = setupMocks();
// Act
wrapper.vm.selectedProvider = "TPAOption";
useMainStore().issConfig.enableTPAFlow = true;
wrapper.vm.forwardButtonAction();
// Test
expect(wrapper.vm.$router.navigate).toBeCalledWith(navigationScenarios.CLICKED_FORWARD_WITH_TPA_ENABLED, "provider-preference");
});
test("Should navigate to tpa disabled route page when TPAOption selected and TPA Flow disabled", () =>
{
// Arrange
const { wrapper } = setupMocks();
// Act
wrapper.vm.selectedProvider = "TPAOption";
useMainStore().issConfig.enableTPAFlow = false;
wrapper.vm.forwardButtonAction();
// Test
expect(wrapper.vm.$router.navigate).toBeCalledWith(navigationScenarios.CLICKED_FORWARD_WITH_TPA_DISABLED, "provider-preference");
});
});
function setupMocks(mockApiResponses){
const mountOptions = getMountOptions({
router: {
navigate: jest.fn(),
},
route: "provider-preference"
});
const wrapper = shallowMount(
ProviderPreference,
mountOptions
);
useMainStore().getCoveragePolicyInfo = jest.fn().mockImplementation(() => {
return Promise.resolve({
data: {},
})
})
const apiResponses = mockApiResponses;
settleAllPromises.mockImplementation(() => apiResponses);
fetchCmsContentForPage.mockImplementation (() => {});
return { wrapper };
}

View file

@ -1,39 +1,41 @@
<template>
<Form @submit="onSubmit"
@invalid-submit="onInvalidSubmit"
ref="theForm"
v-slot="{ meta }" >
@invalid-submit="onInvalidSubmit"
ref="theForm"
>
<div class="page-container-grouped-styles">
<div class="fade-on-route-transition position-relative px-5">
<div class="fade-on-route-transition position-relative">
<siteHeader cmsWidgetName="SiteHeaderWidget" />
<siteSubHeader cmsWidgetName="SiteSubHeader" id="sub-header" class="mt-4"/>
<button v-on:click="forwardButtonAction">Temp Forward Button</button>
<div>
<siteSubHeader cmsWidgetName="SiteSubHeader" id="sub-header" class="mb-5 mt-3" />
<buttonQuestion ref="buttonQuestion"
questionText="Select an option:"
:answers="prefAnswers"
groupName="prefQuestions"
buttonTypeString="providerPrefRadio"
v-model="selectedProvider"
validationRules="option-required"
isRequired
class="mx-5" />
<div class="px-5">
<siteFooter
cmsWidgetName="SiteFooterWidget"
:isForwardActionDisabled="isForwardActionDisabled"
@backClicked="backButtonAction"
@forwardClicked="forwardButtonAction"
ref="siteFooter" />
cmsWidgetName="SiteFooterWidget"
:isForwardActionDisabled="isForwardActionDisabled"
@backClicked="backButtonAction"
@forwardClicked="forwardButtonAction"
ref="siteFooter" />
</div>
</div>
</div>
<contentGroupModal cmsWidgetName="ShopPreferenceDrawer"
ref="ShopPreferenceDrawer"
/>
<modal
ref="SteeringModal"
modalId="SteeringModal"
@footer-button-event="closeSteeringModal"
:footerButtonText="steeringModalFooter">
<h5 class="text-center">{{steeringModalHeader}}</h5>
<div class="steeringModalBody">
<div class="mb-4" >{{steeringModalBody}}</div>
<div v-if="steeringModalBody2">{{steeringModalBody2}}</div>
</div>
</modal>
</Form>
<recalModal ref="recalModal" cmsWidgetName="RecalModal" />
<steeringModal cmsWidgetName="StateSteeringModal" ref="StateSteeringModal" />
<shopPreferenceModal
cmsWidgetName="ShopPreferenceDrawer"
ref="ShopPreferenceDrawer"
@openSteering="openStateSteeringModal"
:showSteeringLink="showSteeringLink"/>
</Form>
</template>
<script>
// Import Supporting Files
@ -43,18 +45,23 @@ import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
import buttonQuestion from "@/digital-components/button-question/button-question";
import contentGroupModal from "@/iss-components/content-group-modal/content-group-modal";
import modal from "@/digital-components/modal/modal.vue"
import { states } from "@/constants/states"
import { issPageValues } from "@/router/router-constants/issPage-values";
// Import Component
import baseFormMixin from "@/mixins/base-form-mixin";
import { Form,defineRule } from "vee-validate";
import siteFooter from "@/iss-components/site-footer/site-footer.vue";
import siteHeader from "@/iss-components/site-header/site-header.vue";
import siteSubHeader from "@/iss-components/site-sub-header/site-sub-header.vue";
import { Form, defineRule } from "vee-validate";
import siteFooter from "@/iss-components/site-footer/site-footer";
import siteHeader from "@/iss-components/site-header/site-header";
import siteSubHeader from "@/iss-components/site-sub-header/site-sub-header";
import providerPrefRadio from "./provider-pref-radio/provider-pref-radio";
import recalModal from "@/layouts/coverage-statement/recal-modal/recal-modal";
import steeringModal from "@/layouts/provider-preference/steering-modal/steering-modal";
import shopPreferenceModal from "@/layouts/provider-preference/shop-preference-modal/shop-preference-modal";
// DEFINE VALIDATION RULES
defineRule("questions-required", required(errorMessages.OPTION_REQUIRED));
defineRule('option-required', required(errorMessages.OPTION_REQUIRED));
const options = { SAFELITE: 'SafeliteOption', TPA: 'TPAOption' };
export default {
name: "provider-preference",
mixins: [baseFormMixin],
@ -66,11 +73,16 @@ export default {
Form,
buttonQuestion,
contentGroupModal,
modal
providerPrefRadio,
recalModal,
steeringModal,
shopPreferenceModal
},
data() {
return {
SelectedshopLocation : null,
selectedProvider : null,
showSteeringLink: false,
tpaAcknowledgement: false,
};
},
async beforeRouteEnter(to, from, next) {
@ -89,67 +101,109 @@ export default {
},
computed: {
isForwardActionDisabled() {
return this.SelectedshopLocation === null;
return this.selectedProvider === null;
},
ProviderPreferenceHeaderText(){
return this.getCmsContent("ProviderPreference", "HeaderText");
},
questionText() {
return this.getCmsContent("ProviderPreference", "SubHeaderText");
},
steeringModalHeader() {
let header = this.getCmsContent("StateSteeringModal", "HeaderText");
return header.replace("{custom:state}", states[this.mainStore.order.customer.address.state])
},
steeringModalBody() {
const bodyText = this.getCmsContent("StateSteeringModal", "BodyText");
 return processIfStatements(bodyText, "custom", this.getStateSpecificText);
},
steeringModalBody2() {
const bodyText = this.getCmsContent("StateSteeringModal", "BodyText2");
 return processIfStatements(bodyText, "custom", this.getStateSpecificText);
},
steeringModalFooter() {
return this.getCmsContent("StateSteeringModal", "FooterText");
prefAnswers() {
const cmsAnswersContent = [
{
cmsWidgetName: options.SAFELITE
},
{
cmsWidgetName: options.TPA
}
];
//if cms content has not yet loaded, skip
if (this.getCmsContent(cmsAnswersContent[0].cmsWidgetName, 'HeaderText') == '') {
return {};
}
const modifiedAnswers = cmsAnswersContent.map((answer) => ({
value: answer.cmsWidgetName,
buttonLabel: this.getHeaderTextFromCms(answer.cmsWidgetName),
buttonLabelSubCopy: this.getSubheaderTextFromCms(answer.cmsWidgetName),
buttonBodyCopy: this.getBodyTextFromCms(answer.cmsWidgetName),
}
));
return modifiedAnswers;
}
},
methods: {
getStateSpecificText(value) {
return this.mainStore.order.customer.address.state?.toLowerCase() == value?.toLowerCase();
getHeaderTextFromCms(cmsWidgetName) {
const headerText = this.getCmsContent(cmsWidgetName, 'HeaderText');
return headerText?.replace("{custom:SafeliteLogo}", "<span class='safeliteLogo'></span>");
},
getSubheaderTextFromCms(cmsWidgetName) {
return this.getCmsContent(cmsWidgetName, 'SubheaderText');
},
getBodyTextFromCms(cmsWidgetName) {
return this.getCmsContent(cmsWidgetName, "BodyText");
},
arePagePrerequisiteValid() {
return true;
},
backButtonAction() {
/**
* this.navigationScenarios comes from base-mixin
*/
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
},
forwardButtonAction() {
//todo
this.$router.navigate(this.navigationScenarios.CLICKED_FORWARD_WITH_SAFELITE, this.$route);
//if(this.SelectedshopLocation == "Schedule with Safelite")
//{
// this.$router.navigate(this.navigationScenarios.CLICKED_FORWARD_WITH_SAFELITE, this.$route);
//}
navigateForward(scenario){
this.$router.navigate(scenario, this.$route);
},
closeSteeringModal() {
this.$refs["SteeringModal"].closeModal();
forwardButtonAction() {
if(this.selectedProvider) {
let scenario = null;
switch (this.selectedProvider) {
case options.SAFELITE:
scenario = this.navigationScenarios.CLICKED_FORWARD_WITH_SAFELITE;
break;
case options.TPA:
if(this.mainStore.issConfig.enableTPAFlow)
{
if(this.mainStore.hasRecalibrationPart)
{
//open tpa adas modal here, else save selection and navigate forward
console.log('add tpa modal code');
return;
}
scenario = this.navigationScenarios.CLICKED_FORWARD_WITH_TPA_ENABLED;
}
else{
scenario = this.navigationScenarios.CLICKED_FORWARD_WITH_TPA_DISABLED;
}
break;
};
this.navigateForward(scenario);
this.mainStore.saveProviderPreferenceData({selectedProvider: this.selectedProvider, tpaAcknowledgement: this.tpaAcknowledgement});
}
},
openStateSteeringModal() {
this.$refs["StateSteeringModal"].openModal();
},
setShowSteeringLink() {
const bodyText = this.getSteeringModalBody();
this.showSteeringLink = !!bodyText;
},
getSteeringModalBody() {
return this.$refs["StateSteeringModal"]?.ModalBodyText;
}
},
mounted() {
setupModalLinks(this);
if(this.steeringModalBody) {
this.$refs["SteeringModal"].openModal();
this.setShowSteeringLink();
const pageData = this.mainStore.pageData(issPageValues.PROVIDER_PREFERENCE);
this.selectedProvider = pageData?.selectedProvider ? pageData.selectedProvider : null;
this.tpaAcknowledgement = pageData?.tpaAcknowledgement ? pageData.tpaAcknowledgement : false;
if(this.showSteeringLink) {
this.openStateSteeringModal();
}
}
};
</script>
<style lang="scss">
<style lang="scss" >
#sub-header span{
color: $black;
}
.modal-link a{
color: $blue-700;
font-size: 14px;
@ -158,9 +212,19 @@ export default {
}
.question-text {
margin-top: 0;
margin-bottom: .5rem;
& > span {
text-align: left;
}
}
.safeliteLogo {
background-image: url(~@/assets/img/icons/safelite-logo.svg);
background-repeat: no-repeat;
background-size: 4.5rem;
margin: 0 0.25rem 0 0.25rem;
padding: 0 2.5rem 0 2.5rem;
}
</style>

View file

@ -0,0 +1,70 @@
import shopPreferenceModal from "./shop-preference-modal.vue"
import { shallowMount } from '@vue/test-utils';
import { settleAllPromises } from "@/helpers/layout-helper.js";
import { fetchCmsContentForPage, setupModalLinks } from "@/helpers/cms-content-helper";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { useMainStore } from "@/store";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
import { nextTick } from "vue";
// Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({
settleAllPromises: jest.fn(),
}));
// Mock fetchCmsContentForPage
jest.mock("@/helpers/cms-content-helper", () => ({
fetchCmsContentForPage: jest.fn(),
setupModalLinks: jest.fn(),
}));
describe('provider-preference.vue', () => {
test("Should display state specific steering modal link when in state with steering language", async () => {
// Arrange
const { wrapper } = setupMocks({showSteeringLink: true});
// Act
// Test
expect(wrapper.vm.showSteeringLink).toBeTruthy();
});
test("Should not display state specific steering modal link when not in state with steering language", async () => {
// Arrange
const { wrapper } = setupMocks({showSteeringLink: false});
// Act
// Test
expect(wrapper.vm.showSteeringLink).toBeFalsy();
});
});
function setupMocks(propsData){
const mountOptions = getMountOptions({
router: {
navigate: jest.fn(),
},
});
mountOptions.propsData = propsData;
const wrapper = shallowMount(
shopPreferenceModal,
mountOptions
);
const apiResponses = {};
settleAllPromises.mockImplementation(() => apiResponses);
fetchCmsContentForPage.mockImplementation (() => {});
return { wrapper };
}

View file

@ -0,0 +1,95 @@
<template>
<modal
:ref="ModalName"
:modalId="ModalName"
:footerButtonText="ModalCloseButtonText"
@footer-button-event="footerButtonClick">
<h5 class="text-center mb-4">{{ModalHeadline}}</h5>
<div>
<div class="mb-4" v-html="ModalBodyText"></div>
<a
class="modal-text"
v-if="showSteeringLink"
v-html="ModalSubBodyText"
@click="openSteeringModal">
</a>
</div>
</modal>
</template>
<script>
import modal from "@/digital-components/modal/modal";
import { states } from "@/constants/states";
export default {
name: "content-group-modal",
props: {
cmsWidgetName: String,
showSteeringLink: Boolean,
},
emits: ["openSteering"],
computed: {
ModalName() {
return this.cmsWidgetName;
},
ModalHeadline() {
return this.getCmsContent(this.cmsWidgetName, "HeaderText");
},
ModalSubheadertext() {
return this.getCmsContent(this.cmsWidgetName, "SubheaderText");
},
ModalBodyText() {
return this.getCmsContent(this.cmsWidgetName, "BodyText");
},
ModalSubBodyText() {
let header = this.getCmsContent(this.cmsWidgetName, "BodyText2");
return header.replace("{custom:state}", states[this.mainStore.order.customer.address.state])
},
ModalImage() {
return this.getCmsContent(this.cmsWidgetName, "Image");
},
ModalCloseButtonText() {
return this.getCmsContent(this.cmsWidgetName, "FooterText");
},
},
methods: {
openModal() {
this.$refs[this.ModalName].openModal();
},
footerButtonClick() {
this.$refs[this.ModalName].closeModal();
},
openSteeringModal(){
this.$refs[this.ModalName].closeModal();
this.$emit("openSteering");
}
},
components: {
modal,
},
};
</script>
<style lang="scss">
.modal {
&.modal-component {
.modal-dialog {
.modal-content {
.modal-body {
.modal-sub-body {
color: $gray-600;
}
ul {
margin-bottom: 0;
}
p {
&:last-child {
margin-bottom: 0;
}
}
}
}
}
}
}
</style>

View file

@ -0,0 +1,52 @@
import steeringModal from "./steering-modal.vue"
import { shallowMount } from '@vue/test-utils';
import { settleAllPromises } from "@/helpers/layout-helper.js";
import { fetchCmsContentForPage, setupModalLinks } from "@/helpers/cms-content-helper";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { useMainStore } from "@/store";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
import { nextTick } from "vue";
// Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({
settleAllPromises: jest.fn(),
}));
// Mock fetchCmsContentForPage
jest.mock("@/helpers/cms-content-helper", () => ({
fetchCmsContentForPage: jest.fn(),
setupModalLinks: jest.fn(),
}));
describe('steering-modal.vue', () => {
test('getStateSpecificText should return true if state matches', () => {
const { wrapper } = setupMocks();
useMainStore().order.customer.address.state = "OH";
expect(wrapper.vm.getStateSpecificText("OH")).toBeTruthy();
})
});
function setupMocks(){
const mountOptions = getMountOptions({
router: {
navigate: jest.fn(),
},
});
const wrapper = shallowMount(
steeringModal,
mountOptions
);
const apiResponses = {};
settleAllPromises.mockImplementation(() => apiResponses);
fetchCmsContentForPage.mockImplementation (() => {});
return { wrapper };
}

View file

@ -0,0 +1,92 @@
<template>
<modal
:ref="ModalName"
:modalId="ModalName"
:footerButtonText="ModalCloseButtonText"
@footer-button-event="footerButtonClick">
<h5 class="text-center mb-4">{{ModalHeadline}}</h5>
<div>
<div class="mb-4" v-html="ModalBodyText"></div>
<div v-if="ModalSubBodyText" v-html="ModalSubBodyText"></div>
</div>
</modal>
</template>
<script>
import modal from "@/digital-components/modal/modal";
import { processIfStatements } from '@/helpers/cms-content-helper';
import { states } from "@/constants/states";
export default {
name: "content-group-modal",
props: {
cmsWidgetName: String,
},
computed: {
ModalName() {
return this.cmsWidgetName;
},
ModalHeadline() {
let header = this.getCmsContent(this.cmsWidgetName, "HeaderText");
return header.replace("{custom:state}", states[this.mainStore.order.customer.address.state])
},
ModalSubheadertext() {
return this.getCmsContent(this.cmsWidgetName, "SubheaderText");
},
ModalBodyText() {
const bodyText = this.getCmsContent(this.cmsWidgetName, "BodyText");
return processIfStatements(bodyText, "custom", this.getStateSpecificText);
},
ModalSubBodyText() {
const bodyText = this.getCmsContent(this.cmsWidgetName, "BodyText2");
return processIfStatements(bodyText, "custom", this.getStateSpecificText);
},
ModalImage() {
return this.getCmsContent(this.cmsWidgetName, "Image");
},
ModalCloseButtonText() {
return this.getCmsContent(this.cmsWidgetName, "FooterText");
},
},
methods: {
openModal() {
this.$refs[this.ModalName].openModal();
},
footerButtonClick() {
this.$refs[this.ModalName].closeModal();
},
getStateSpecificText(value) {
return this.mainStore.order.customer.address.state?.toLowerCase() == value?.toLowerCase();
},
},
components: {
modal,
},
};
</script>
<style lang="scss">
.modal {
&.modal-component {
.modal-dialog {
.modal-content {
.modal-body {
.modal-sub-body {
color: $gray-600;
}
ul {
margin-bottom: 0;
}
p {
&:last-child {
margin-bottom: 0;
}
}
}
}
}
}
}
</style>

View file

@ -322,9 +322,6 @@ export default {
</script>
<style lang="scss">
.my-2 {
margin-bottom: 0rem !important;
}
.page-container-grouped-styles.welcome {
height: auto;
}

View file

@ -4,6 +4,7 @@ export const issPageValues = {
ADDRESS_LOOKUP: 'address-lookup',
ADDRESS_VEHICLES: 'address-vehicles',
BAILOUT_PAGE: 'bailout-page',
CAPABILITY_QUESTIONS: 'capability-questions',
CONTACT_DETAILS: 'contact-details',
ESTIMATE: 'estimate',

View file

@ -45,7 +45,8 @@ const navigationScenarios = {
//Provider Preference
CLICKED_FORWARD_WITH_SAFELITE: "CLICKED_FORWARD_WITH_SAFELITE",
CLICKED_FORWARD_WITH_TPA: "CLICKED_FORWARD_WITH_TPA"
CLICKED_FORWARD_WITH_TPA_ENABLED: "CLICKED_FORWARD_WITH_TPA_ENABLED",
CLICKED_FORWARD_WITH_TPA_DISABLED: "CLICKED_FORWARD_WITH_TPA_DISABLED"
};

View file

@ -440,6 +440,14 @@ const routingTable = function(store) {
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_SAFELITE,
destinationIssPageValue: issPageValues.SERVICE_LOCATION
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_TPA_ENABLED,
destinationIssPageValue: issPageValues.TPA_SEARCH
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_TPA_DISABLED,
destinationIssPageValue: issPageValues.BAILOUT_PAGE
}
]
},

View file

@ -82,7 +82,7 @@ const getDefaultState = () => {
}
},
referralNumber: null,
referralDate: null
referralDate: null,
},
applicationUser: {
experiments: [],
@ -118,6 +118,7 @@ export const useMainStore = defineStore({
id: storeId,
state: () => state,
getters: {
hasRecalibrationPart: (state) => getHasRecalibrationPart(state),
vehicle: (state) => state.order.vehicle,
damage: (state) => state.order.damage,
lineItems: (state) => state.order.lineItems,
@ -944,6 +945,9 @@ export const useMainStore = defineStore({
saveVaps(vaps) {
this.order.lineItems.vaps = vaps;
},
saveProviderPreferenceData(data){
this.updatePageData({ page: issPageValues.PROVIDER_PREFERENCE, data: data});
},
addEventToBus (event) {
this.applicationUser.eventBus.push(event);
},

View file

@ -4,7 +4,8 @@ html {
&.list-button-horizontal,
&.list-button,
&.list-button.list-group,
&.list-card {
&.list-card
&.option-label {
border: 1px solid $red;
position: relative;
z-index: 4;