Merge pull request #1086 from Safelite/feature/CSR-1112

Feature/csr 1112
This commit is contained in:
Leah Schumann 2023-05-01 16:56:19 -04:00 committed by GitHub
commit 55b8c5061f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 1143 additions and 8 deletions

View file

@ -90,6 +90,10 @@ const endpoints = {
url: "/parts/api/v1/parts/supporting-items",
method: "POST",
},
GetProviderLocations: {
url: "/location/api/v1/location/providers",
method: "GET",
},
GetCapabilityQuestions: {
url: "/parts/api/v1/parts/capability-questions",
method: "GET",

View file

@ -31,6 +31,7 @@ const storeActions = {
GET_MOLDING_QUESTIONS: "getMoldingQuestions",
GET_MOBILE_FEE_PART: "getMobileFeePart",
GET_SERVICEABILITY_DETAILS: "getServiceabilityDetails",
GET_PROVIDER_LOCATIONS: "getProviderLocations",
SAVE_SESSION: "saveSession",
LOAD_SESSION: "loadSession",
UPDATE_STORE_WITH_SAVE_SESSION_RESPONSE: "updateStoreWithSaveSessionResponse",

View file

@ -129,6 +129,7 @@ export default {
valueToLogType: String,
additionalButtonStyling: String,
isSmallQuestionText: Boolean,
availability: String,
customButtonQuestionId: String,
logDisplayedValuesEvent: {
type: Boolean,
@ -255,8 +256,10 @@ export default {
},
},
watch: {
modelValue() {
this.resetField();
modelValue(newValue) {
this.resetField({
value: newValue,
});
},
answers() {
//once we get the answers to display from parent, see if we need a GA event to log what we showed

View file

@ -2,6 +2,7 @@
<transition name="fade" mode="out-in">
<div class="appointment-type-question" aria-live="polite">
<buttonQuestion
ref="buttonQuestion"
customButtonQuestionId="appointmentTypeQuestion"
:questionText="questionText"
:answers="answersToDisplay"
@ -82,8 +83,6 @@ export default {
handler(newValue) {
if (newValue) {
this.selectedValues = "Mobile";
} else {
this.selectedValues = null;
}
},
},

View file

@ -190,6 +190,8 @@ describe("service-location.vue", () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn();
const mobileFeePart = {
partNumber: "MOBILE FEE",
description: "MOBILE FEE",
@ -585,6 +587,8 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn();
// Act
await serviceLocation.beforeRouteEnter.call(
wrapper.vm,
@ -611,6 +615,8 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn();
// Act
await serviceLocation.beforeRouteEnter.call(
wrapper.vm,
@ -637,6 +643,8 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn();
// Act
await serviceLocation.beforeRouteEnter.call(
wrapper.vm,
@ -663,6 +671,8 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn();
// Act
await serviceLocation.beforeRouteEnter.call(
wrapper.vm,
@ -689,6 +699,8 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn();
// Act
await serviceLocation.beforeRouteEnter.call(
wrapper.vm,
@ -716,6 +728,8 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn();
// Act
await serviceLocation.beforeRouteEnter.call(
wrapper.vm,
@ -743,6 +757,8 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn();
// Act
await serviceLocation.beforeRouteEnter.call(
wrapper.vm,
@ -770,6 +786,8 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn();
// Act
await serviceLocation.beforeRouteEnter.call(
wrapper.vm,
@ -797,6 +815,8 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn();
// Act
await serviceLocation.beforeRouteEnter.call(
wrapper.vm,
@ -824,6 +844,8 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn();
// Act
await serviceLocation.beforeRouteEnter.call(
wrapper.vm,
@ -851,6 +873,8 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn();
// Act
await serviceLocation.beforeRouteEnter.call(
wrapper.vm,
@ -878,6 +902,8 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn();
// Act
await serviceLocation.beforeRouteEnter.call(
wrapper.vm,
@ -905,6 +931,8 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn();
// Act
await serviceLocation.beforeRouteEnter.call(
wrapper.vm,
@ -932,6 +960,8 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn();
// Act
await serviceLocation.beforeRouteEnter.call(
wrapper.vm,
@ -959,6 +989,8 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn();
// Act
await serviceLocation.beforeRouteEnter.call(
wrapper.vm,
@ -989,6 +1021,8 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn();
// Act
await serviceLocation.beforeRouteEnter.call(
wrapper.vm,
@ -1015,6 +1049,8 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn();
// Act
await serviceLocation.beforeRouteEnter.call(
wrapper.vm,
@ -1041,6 +1077,8 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn();
// Act
await serviceLocation.beforeRouteEnter.call(
wrapper.vm,
@ -1068,6 +1106,8 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn();
// Act
await serviceLocation.beforeRouteEnter.call(
wrapper.vm,
@ -1095,6 +1135,8 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn();
// Act
await serviceLocation.beforeRouteEnter.call(
wrapper.vm,
@ -1122,6 +1164,8 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn();
// Act
await serviceLocation.beforeRouteEnter.call(
wrapper.vm,
@ -1151,6 +1195,8 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn();
// Act
await serviceLocation.beforeRouteEnter.call(
wrapper.vm,
@ -1178,6 +1224,8 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn();
// Act
await serviceLocation.beforeRouteEnter.call(
wrapper.vm,
@ -1205,6 +1253,8 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn();
// Act
await serviceLocation.beforeRouteEnter.call(
wrapper.vm,
@ -1232,6 +1282,8 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn();
// Act
await serviceLocation.beforeRouteEnter.call(
wrapper.vm,
@ -1259,6 +1311,8 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn();
// Act
await serviceLocation.beforeRouteEnter.call(
wrapper.vm,
@ -1286,6 +1340,8 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn();
// Act
await serviceLocation.beforeRouteEnter.call(
wrapper.vm,
@ -1313,6 +1369,8 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn();
// Act
await serviceLocation.beforeRouteEnter.call(
wrapper.vm,
@ -1342,6 +1400,8 @@ describe("service-location.vue", () => {
const { wrapper } = setupMocks({});
wrapper.vm.$refs.shopQuestion.initializeComponent = jest.fn();
// Act
await serviceLocation.beforeRouteEnter.call(
wrapper.vm,

View file

@ -46,7 +46,7 @@
alertClass="alert-warning" />
<appointmentTypeQuestion
v-model="selectedAppointmentType"
v-if="!displayNoShopsAlert"
v-show="!displayNoShopsAlert"
:isServiceableMobile="isServiceableMobile"
:isServiceableInshop="isServiceableInshop"
ref="appointmentTypeQuestion"
@ -65,6 +65,19 @@
ref="mobileLocationQuestions"
linkWidgetName="MobileLocationLinkWidget"
modalWidgetName="MobileLocationModalWidget" />
<Transition name="fade" mode="out-in">
<shopQuestion
ref="shopQuestion"
v-show="
selectedAppointmentType === 'Inshop' ||
selectedAppointmentType === 'Dropoff'
"
v-model="providerNumber"
:serviceZipCode="zipCode"
:selectedAppointmentType="selectedAppointmentType"
:shopQuestionData="shopQuestionInitialData"
cmsWidgetName="ShopQuestionWidget" />
</Transition>
<contentGroupModal ref="RecalModal" cmsWidgetName="RecalModal" />
<funnel-footer
cmsWidgetName="FunnelFooterWidget"
@ -82,6 +95,8 @@ import alert from "@/ux-components/alert/alert";
import serviceZipModalQuestion from "@/layouts/service-location/service-zip-modal-question/service-zip-modal-question";
import mobileLocationModalQuestions from "@/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions";
import appointmentTypeQuestion from "@/layouts/service-location/appointment-type-question/appointment-type-question";
import shopQuestion from "@/layouts/service-location/shop-question/shop-question";
import funnelHeader from "@/fmg-components/funnel-header/funnel-header";
import funnelFooter from "@/fmg-components/funnel-footer/funnel-footer";
import funnelSubHeader from "@/fmg-components/funnel-sub-header/funnel-sub-header";
@ -133,10 +148,10 @@ export default {
isRecalibrationServiceableInshop: null,
isGlassServiceableMobile: null,
isRecalibrationServiceableMobile: null,
selectedAppointmentType: null,
providerNumber: null,
mobileFeePart: null,
zipContainsMilitaryBase: false,
selectedAppointmentType: "",
providerNumber: null,
};
},
async beforeRouteEnter(to, from, next) {
@ -150,6 +165,8 @@ export default {
const mobileFeePartPromise = getPricedMobileFeePart(serviceZipCode);
const shopQuestionInitialDataPromise = shopQuestion.methods.loadInitialData();
// Settle promises and get results
const promiseResultMap = [
{
@ -168,6 +185,10 @@ export default {
resultKey: "zipCodeData",
promise: getZipCodeData,
},
{
resultKey: "shopQuestionInitialData",
promise: shopQuestionInitialDataPromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap);
@ -180,6 +201,7 @@ export default {
resultMap.serviceabilityDetails,
resultMap.mobileFeePart
);
vm.$refs.shopQuestion.initializeComponent(resultMap.shopQuestionInitialData);
});
},
computed: {
@ -274,6 +296,12 @@ export default {
displayNoShopsAlert() {
return !this.isServiceableInshop && !this.isServiceableMobile;
},
isDropoff() {
return this.selectedAppointmentType === "Dropoff";
},
isMobile() {
return this.selectedAppointmentType === "Mobile";
},
},
methods: {
arePagePrerequisitesValid() {
@ -283,7 +311,7 @@ export default {
store.getters.payment.isInsurance !== null
);
},
setData(zipCodeData, serviceabilityDetails, mobileFeePart) {
setData(zipCodeData, serviceabilityDetails, mobileFeePart, shopQuestionData) {
if (zipCodeData) {
this.zipContainsMilitaryBase = zipCodeData.containsMilitaryBase;
}
@ -355,6 +383,7 @@ export default {
Form,
loadingModal,
contentGroupModal,
shopQuestion,
},
};
</script>

View file

@ -0,0 +1,70 @@
import { mount } from "@vue/test-utils";
import shopListButton from "./shop-list-button";
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
describe("service-package-radio.vue", () => {
it("Should include buttonLabel in html", async () => {
// Arrange
let { wrapper } = setupMocks({
mountOptionsMockData: {
propsData: mockProps,
},
});
// Act
const outputHtml = wrapper.html();
// Assert
expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonLabel"]));
});
it("Should include buttonLabelSubCopy in html", async () => {
// Arrange
let { wrapper } = setupMocks({
mountOptionsMockData: {
propsData: mockProps,
},
});
// Act
const outputHtml = wrapper.html();
// Assert
expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonLabelSubCopy"]));
});
it("Should include buttonLabelAuxillaryCopy in html", async () => {
// Arrange
let { wrapper } = setupMocks({
mountOptionsMockData: {
propsData: mockProps,
},
});
// Act
const outputHtml = wrapper.html();
// Assert
expect(outputHtml).toEqual(expect.stringContaining(mockProps["buttonLabelAuxillaryCopy"]));
});
});
const mockProps = {
buttonLabel: "buttonLabel test copy",
buttonLabelSubCopy: "buttonLabelSubCopy test copy",
buttonBodyCopy:
"<ul><li>buttonBodyCopy test copy</li><li>2</li><li>3</li><li>4</li><li>5</li></ul>",
buttonLabelAuxillaryCopy: "buttonLabelAuxillaryCopy test copy",
value: 0,
modelValue: 0,
groupName: "mockGroup",
};
function setupMocks({ mountOptionsMockData = {} }) {
const wrapper = mount(shopListButton, {
...mountOptionsMockData,
mixins: [inputButtonWrapperMixin],
});
return { wrapper };
}

View file

@ -0,0 +1,179 @@
<template>
<transition name="fade" mode="out-in">
<baseInputButton
v-bind="$props"
buttonWrapperClasses="list-group base-input-button list-button rounded-3 d-flex flex-column w-100 mb-2"
v-model="selectedValue">
<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 class="m-0 button-label-copy" :class="textPosition">{{
buttonLabel
}}</span>
<span class="m-0 button-label-sub-copy" :class="textPosition">{{
buttonLabelSubCopy
}}</span>
<div
class="availability-indicator"
:class="availability === 'high' ? 'green' : 'red'">
<span class="m-0 button-auxillary-copy">{{ buttonAuxillaryCopy }}</span>
</div>
</div>
<span
v-if="buttonBodyCopy"
class="m-0 button-label-sub-copy small"
:class="textPosition"
v-html="buttonBodyCopy"></span>
<span v-if="screenReaderOnlyText" class="sr-only">
{{ screenReaderOnlyText }}
</span>
<loader
v-if="isLoaderDisplayed && selectingInitiatesLoad"
:class="[this.loaderColor, this.loaderPosition]" />
</div>
</baseInputButton>
</transition>
</template>
<script>
import loader from "@/ux-components/loader/loader";
import baseInputButton from "@/digital-components/base-input-button/base-input-button";
import inputButtonWrapperMixin from "@/mixins/input-button-wrapper-mixin";
export default {
name: "shopListButton",
mixins: [inputButtonWrapperMixin],
props: {
loaderColor: String,
loaderPosition: {
type: String,
default: "right",
},
},
data() {
return {
isLoaderDisplayed: false,
availability: "low",
};
},
methods: {
displayLoader() {
this.isLoaderDisplayed = true;
},
preHandleAnswerChange() {
if (this.selectingInitiatesLoad) {
this.displayLoader();
}
},
},
components: {
loader,
baseInputButton,
},
};
</script>
<style lang="scss" scoped>
.loader {
position: absolute;
}
.list-button {
outline: none;
input[type="radio"],
input[type="checkbox"] {
position: static; //override bootstrap
&:focus-visible + .list-button-content {
box-shadow: 0 0 0 2.5px $blue;
}
&:focus + .list-button-content {
box-shadow: 0 0 0 2.5px $blue;
}
&:checked + .list-button-content {
color: $black;
font-weight: 500;
background: $blue-100;
box-shadow: 0 0 0 1px $blue;
}
&:checked:focus + .list-button-content {
box-shadow: 0 0 0 2.5px $blue;
}
&:checked + .list-button-content p,
&:checked + .list-button-content span {
font-weight: 500;
}
&:checked + .list-button-content span:nth-child(2) {
font-weight: 400;
color: $gray-600;
}
}
}
.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-one {
display: flex;
align-items: center;
margin-bottom: 0.25rem !important;
.button-label-copy {
flex-grow: 0;
line-height: 1.5rem;
font-weight: 500;
}
.button-label-sub-copy {
flex-grow: 1;
line-height: 1.25rem !important;
font-weight: 400;
font-size: 0.75rem;
color: #727676;
padding-left: 0.25rem;
}
.availability-indicator {
display: none;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 0.125rem 1.5rem;
gap: 0.25rem;
background: #e3f2ea;
border-radius: 4.5rem;
.button-auxillary-copy {
justify-content: right;
line-height: 1.25rem !important;
font-weight: 400;
font-size: 0.75rem;
}
.green {
color: #006a36;
background: #e3f2ea;
}
.red {
color: #ac160b;
background: #e3f2ea;
}
}
}
}
</style>

View file

@ -0,0 +1,554 @@
import { shallowMount } from "@vue/test-utils";
import { storeActions } from "@/constants/store-actions";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import shopQuestion from "./shop-question";
jest.mock("@/mixins/base-mixin", () => ({
methods: {
dispatchStoreAction(action, items, encode) {
if (action === mockGetProviderLocationsStoreAction) {
return new Promise((resolve) => {
resolve(mockNewShopList);
});
}
},
},
}));
const mockGetProviderLocationsStoreAction = storeActions.GET_PROVIDER_LOCATIONS;
const mockNewShopList = {
data: [
{
city: "Far",
country: "United States",
distance: 100,
providerNumber: "129",
state: "OH",
streetAddress: "555 First Capital Ln",
zipCode: "45601",
},
{
city: "Farther",
country: "United States",
distance: 200,
providerNumber: "130",
state: "OH",
streetAddress: "5486 N Grove Rd",
zipCode: "43215",
},
{
city: "Farthest (Ever)",
country: "United States",
distance: 380.5,
providerNumber: "131",
state: "OH",
streetAddress: "1670 Bongo Ave D",
zipCode: "43223",
},
],
};
const mockCmsContent = {
QuestionText: "Select a shop:",
};
const cmsWidgetName = "AppointmentTypeQuestionWidget";
const shopQuestionInitialData = [
{
city: "Worthington",
country: "United States",
distance: 1.5,
providerNumber: "123",
state: "OH",
streetAddress: "760 Dearborn Park Ln",
zipCode: "43085",
},
{
city: "Columbus",
country: "United States",
distance: 4.5,
providerNumber: "124",
state: "OH",
streetAddress: "5486 N Hamilton Rd",
zipCode: "43230",
},
{
city: "Powell",
country: "United States",
distance: 7,
providerNumber: "125",
state: "OH",
streetAddress: "1670 Harmon Ave C",
zipCode: "43223",
},
{
city: "Chillicothe",
country: "United States",
distance: 41.5,
providerNumber: "126",
state: "OH",
streetAddress: "555 First Capital Ln",
zipCode: "45601",
},
{
city: "Grove City",
country: "United States",
distance: 4.5,
providerNumber: "127",
state: "OH",
streetAddress: "5486 N Grove Rd",
zipCode: "43215",
},
{
city: "Dayton",
country: "United States",
distance: 80.5,
providerNumber: "128",
state: "OH",
streetAddress: "1670 Bongo Ave D",
zipCode: "43223",
},
{
city: "Far",
country: "United States",
distance: 100,
providerNumber: "129",
state: "OH",
streetAddress: "555 First Capital Ln",
zipCode: "45601",
},
{
city: "Farther",
country: "United States",
distance: 200,
providerNumber: "130",
state: "OH",
streetAddress: "5486 N Grove Rd",
zipCode: "43215",
},
{
city: "Farthest (Ever)",
country: "United States",
distance: 380.5,
providerNumber: "131",
state: "OH",
streetAddress: "1670 Bongo Ave D",
zipCode: "43223",
},
];
const mockMixin = {
methods: {
getCmsContent: jest.fn((widgetName, cmsFieldName) => {
if (widgetName === cmsWidgetName) {
return mockCmsContent[cmsFieldName];
}
return null;
}),
},
};
describe("shop-question.vue", () => {
it("Should display first three shops when an appointment type has already been selected", async () => {
// Arrange/Act
const container = document.createElement("div");
container.scrollTo = jest.fn();
container.classList.add("page-container-grouped-styles");
document.body.appendChild(container);
const { wrapper } = setupMocks({
mixins: [mockMixin],
props: {
modelValue: null,
serviceZipCode: "43081",
selectedAppointmentType: "Dropoff",
cmsWidgetName: cmsWidgetName,
},
mountOptions: {
attachTo: document.body,
},
});
wrapper.vm.$refs.buttonQuestion.resetField = jest.fn();
wrapper.vm.initializeComponent(shopQuestionInitialData);
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.answers).toEqual([
{
Name: "123",
buttonLabel: "Worthington",
buttonLabelSubCopy: "1.5 mi",
buttonBodyCopy: "760 Dearborn Park Ln, Worthington, OH 43085",
},
{
Name: "124",
buttonLabel: "Columbus",
buttonLabelSubCopy: "4.5 mi",
buttonBodyCopy: "5486 N Hamilton Rd, Columbus, OH 43230",
},
{
Name: "125",
buttonLabel: "Powell",
buttonLabelSubCopy: "7 mi",
buttonBodyCopy: "1670 Harmon Ave C, Powell, OH 43223",
},
]);
});
it("Should display the 'Show more locations' link when there are more than three locations to chose from", async () => {
// Arrange/Act
const container = document.createElement("div");
container.scrollTo = jest.fn();
container.classList.add("page-container-grouped-styles");
document.body.appendChild(container);
const { wrapper } = setupMocks({
mixins: [mockMixin],
props: {
modelValue: null,
serviceZipCode: "43081",
selectedAppointmentType: "Dropoff",
cmsWidgetName: cmsWidgetName,
},
mountOptions: {
attachTo: document.body,
},
});
wrapper.vm.$refs.buttonQuestion.resetField = jest.fn();
wrapper.vm.initializeComponent(shopQuestionInitialData);
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
const showMoreShopsLink = wrapper.findComponent({ ref: "showMoreShopsLink" });
// Assert
expect(showMoreShopsLink.exists()).toBe(true);
});
it("Should not display the 'Show more locations' link when there are fewer than three locations to chose from", async () => {
// Arrange/Act
const container = document.createElement("div");
container.scrollTo = jest.fn();
container.classList.add("page-container-grouped-styles");
document.body.appendChild(container);
const alsoShopQuestionInitialData = [
{
city: "Worthington",
country: "United States",
distance: 1.5,
providerNumber: "123",
state: "OH",
streetAddress: "760 Dearborn Park Ln",
zipCode: "43085",
},
{
city: "Columbus",
country: "United States",
distance: 4.5,
providerNumber: "124",
state: "OH",
streetAddress: "5486 N Hamilton Rd",
zipCode: "43230",
},
{
city: "Powell",
country: "United States",
distance: 7,
providerNumber: "125",
state: "OH",
streetAddress: "1670 Harmon Ave C",
zipCode: "43223",
},
];
const { wrapper } = setupMocks({
mixins: [mockMixin],
props: {
modelValue: null,
serviceZipCode: "43081",
selectedAppointmentType: "Dropoff",
cmsWidgetName: cmsWidgetName,
},
mountOptions: {
attachTo: document.body,
},
});
wrapper.vm.$refs.buttonQuestion.resetField = jest.fn();
wrapper.vm.initializeComponent(alsoShopQuestionInitialData);
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
const showMoreShopsLink = wrapper.findComponent({ ref: "showMoreShopsLink" });
// Assert
expect(showMoreShopsLink.exists()).toBe(false);
});
it("Should display the next three shops when the 'Show more location' link is clicked", async () => {
const container = document.createElement("div");
container.scrollTo = jest.fn();
container.classList.add("page-container-grouped-styles");
document.body.appendChild(container);
const displayedAnswers = [
{
Name: "123",
buttonLabel: "Worthington",
buttonLabelSubCopy: "1.5 mi",
buttonBodyCopy: "760 Dearborn Park Ln, Worthington, OH 43085",
},
{
Name: "124",
buttonLabel: "Columbus",
buttonLabelSubCopy: "4.5 mi",
buttonBodyCopy: "5486 N Hamilton Rd, Columbus, OH 43230",
},
{
Name: "125",
buttonLabel: "Powell",
buttonLabelSubCopy: "7 mi",
buttonBodyCopy: "1670 Harmon Ave C, Powell, OH 43223",
},
];
// Arrange/Act
const { wrapper } = setupMocks({
mixins: [mockMixin],
props: {
modelValue: null,
serviceZipCode: "43081",
selectedAppointmentType: "Dropoff",
cmsWidgetName: cmsWidgetName,
},
mountOptions: {
attachTo: document.body,
},
});
wrapper.vm.$refs.buttonQuestion.resetField = jest.fn();
wrapper.vm.shops = shopQuestionInitialData;
wrapper.vm.answers = displayedAnswers;
wrapper.vm.shopIndex = 3;
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
const showMoreShopsLink = wrapper.findComponent({ ref: "showMoreShopsLink" });
showMoreShopsLink.trigger("click");
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.answers.length).toBe(6);
expect(wrapper.vm.answers).toEqual([
{
Name: "123",
buttonLabel: "Worthington",
buttonLabelSubCopy: "1.5 mi",
buttonBodyCopy: "760 Dearborn Park Ln, Worthington, OH 43085",
},
{
Name: "124",
buttonLabel: "Columbus",
buttonLabelSubCopy: "4.5 mi",
buttonBodyCopy: "5486 N Hamilton Rd, Columbus, OH 43230",
},
{
Name: "125",
buttonLabel: "Powell",
buttonLabelSubCopy: "7 mi",
buttonBodyCopy: "1670 Harmon Ave C, Powell, OH 43223",
},
{
Name: "126",
buttonLabel: "Chillicothe",
buttonLabelSubCopy: "41.5 mi",
buttonBodyCopy: "555 First Capital Ln, Chillicothe, OH 45601",
},
{
Name: "127",
buttonLabel: "Grove City",
buttonLabelSubCopy: "4.5 mi",
buttonBodyCopy: "5486 N Grove Rd, Grove City, OH 43215",
},
{
Name: "128",
buttonLabel: "Dayton",
buttonLabelSubCopy: "80.5 mi",
buttonBodyCopy: "1670 Bongo Ave D, Dayton, OH 43223",
},
]);
});
it("Should display the number of shops necessary to show a previously selected shop", async () => {
// Arrange/Act
const container = document.createElement("div");
container.scrollTo = jest.fn();
container.classList.add("page-container-grouped-styles");
document.body.appendChild(container);
const { wrapper } = setupMocks({
mixins: [mockMixin],
props: {
modelValue: "127",
serviceZipCode: "43081",
selectedAppointmentType: "Dropoff",
cmsWidgetName: cmsWidgetName,
},
mountOptions: {
attachTo: document.body,
},
});
wrapper.vm.$refs.buttonQuestion.resetField = jest.fn();
wrapper.vm.initializeComponent(shopQuestionInitialData);
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.answers.length).toEqual(5);
});
it("Should reset the answers when the selected appointment type changes", async () => {
// Arrange/Act
const container = document.createElement("div");
container.scrollTo = jest.fn();
container.classList.add("page-container-grouped-styles");
document.body.appendChild(container);
const { wrapper } = setupMocks({
mixins: [mockMixin],
props: {
modelValue: "127",
serviceZipCode: "43081",
selectedAppointmentType: "Dropoff",
cmsWidgetName: cmsWidgetName,
},
mountOptions: {
attachTo: document.body,
},
});
wrapper.vm.$refs.buttonQuestion.resetField = jest.fn();
wrapper.vm.initializeComponent(shopQuestionInitialData);
wrapper.setProps({
selectedAppointmentType: "Inshop",
});
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.answers.length).toEqual(3);
});
it("Should reload the shops when the service zip code changes", async () => {
// Arrange/Act
const container = document.createElement("div");
container.scrollTo = jest.fn();
container.classList.add("page-container-grouped-styles");
document.body.appendChild(container);
const { wrapper } = setupMocks({
mixins: [mockMixin],
props: {
modelValue: "127",
serviceZipCode: "43081",
selectedAppointmentType: "Dropoff",
cmsWidgetName: cmsWidgetName,
},
mountOptions: {
attachTo: document.body,
},
});
wrapper.vm.$refs.buttonQuestion.resetField = jest.fn();
wrapper.vm.shops = shopQuestionInitialData;
await wrapper.vm.$nextTick();
wrapper.vm.$options.methods.loadInitialData = jest.fn().mockImplementation(() => {
return new Promise((resolve) => {
resolve(newShopList);
});
});
await wrapper.vm.$options.watch.serviceZipCode.handler.call(wrapper.vm, "43054");
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.shops.length).toEqual(3);
expect(wrapper.vm.shops).toEqual(mockNewShopList.data);
});
it("Should clear the existing answers when the service zip code changes", async () => {
// Arrange/Act
const container = document.createElement("div");
container.scrollTo = jest.fn();
container.classList.add("page-container-grouped-styles");
document.body.appendChild(container);
const { wrapper } = setupMocks({
mixins: [mockMixin],
props: {
modelValue: "127",
serviceZipCode: "43081",
selectedAppointmentType: "Dropoff",
cmsWidgetName: cmsWidgetName,
},
mountOptions: {
attachTo: document.body,
},
});
wrapper.vm.$refs.buttonQuestion.resetField = jest.fn();
wrapper.vm.initializeComponent(shopQuestionInitialData);
wrapper.setProps({
selectedAppointmentType: "Inshop",
});
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.answers.length).toEqual(3);
});
});
function setupMocks({ mountOptions, mixins, props, isShallowMount = true }) {
const resultingMountOptions = getMountOptions({
...mountOptions,
mixins,
});
if (props) resultingMountOptions.propsData = props;
const wrapper = isShallowMount
? shallowMount(shopQuestion, resultingMountOptions)
: mount(shopQuestion, resultingMountOptions);
return { wrapper };
}

View file

@ -0,0 +1,228 @@
<template>
<transition name="fade" mode="out-in">
<div class="shop-question" aria-live="polite">
<alert
ref="alertDropoffInformation"
v-if="displayDropoffInformation"
class="mb-4 drop-off-alert"
cmsWidgetName="AlertDropoffInformationWidget"
alertClass="alert-info"
v-bind:isDismissible="false" />
<buttonQuestion
ref="buttonQuestion"
buttonTypeString="shopListButton"
:buttonTypeObject="shopListButton"
class="radioQuestion"
:questionText="questionText"
:answers="answers"
groupName="chooseShop"
textPosition="text-start"
v-model="selectedValue"
isRequired
validationRules="option-required" />
<textLink
v-if="displaySeeMoreLocationsLink"
ref="showMoreShopsLink"
class="show-more-shops-link"
id="showMoreShopsId"
cmsWidgetName="ShowMoreShopsLinkWidget"
linkType="text"
:text="showMoreShopsLinkText"
href="#!"
@click-event="getNextShopsFromList"
:aria-label="showMoreShopsLinkText" />
</div>
</transition>
</template>
<script>
// Components
import alert from "@/ux-components/alert/alert";
import buttonQuestion from "@/digital-components/button-question/button-question";
import shopListButton from "./shop-list-button/shop-list-button";
import textLink from "@/ux-components/text-link/text-link";
import { storeActions } from "@/constants/store-actions.js";
// Supporting files
import { defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
import baseMixin from "@/mixins/base-mixin.js";
defineRule("option-required", required(errorMessages.OPTION_REQUIRED));
export default {
name: "shop-question",
mixins: [baseMixin],
data() {
return {
shops: [],
shopListButton: shopListButton,
availability: "high",
answers: [],
shopIndex: 0,
displaySeeMoreLocationsLink: false,
};
},
props: {
modelValue: String,
serviceZipCode: String,
selectedAppointmentType: String,
cmsWidgetName: String,
validationRules: String,
},
computed: {
questionText() {
return this.getCmsContent(this.cmsWidgetName, "QuestionText");
},
selectedValue: {
get: function () {
return this.modelValue;
},
set: function (newValue) {
this.$emit("update:modelValue", newValue);
},
},
displayDropoffInformation() {
return this.selectedAppointmentType == "Dropoff";
},
showMoreShopsLinkText() {
return this.getCmsContent("ShowMoreShopsLinkWidget", "Text");
},
},
methods: {
loadInitialData() {
return baseMixin.methods.dispatchStoreAction(storeActions.GET_PROVIDER_LOCATIONS, {
serviceZipCode: this.serviceZipCode,
});
},
initializeComponent(shopQuestionInitialData) {
this.shops = shopQuestionInitialData;
},
async getNextShopsFromList(numberToGet = 3) {
const shopIterator = (array, n) => {
let l = array.length;
return () => {
var end = this.shopIndex + n;
var part = array.slice(this.shopIndex, end);
this.shopIndex = end < l ? end : this.shops.length;
return part;
};
};
const nextShop = shopIterator(this.shops, numberToGet);
// Map API result data
const mappedData = nextShop().map((shop) => {
return {
Name: shop.providerNumber,
buttonLabel: shop.city,
buttonLabelSubCopy: `${shop.distance} mi`,
buttonBodyCopy: `${shop.streetAddress}, ${shop.city}, ${shop.state} ${shop.zipCode}`,
//buttonAuxillaryCopy: `foo`,
};
});
if (this.answers.length === 0) {
this.answers = mappedData;
} else {
mappedData.forEach((shop) => {
this.answers.push(shop);
});
}
await this.$nextTick();
if (this.shopIndex == this.shops.length) {
this.displaySeeMoreLocationsLink = false;
} else {
this.displaySeeMoreLocationsLink = true;
}
await this.$nextTick();
this.scrollToBottom();
},
resetShopList() {
this.answers = [];
this.shopIndex = 0;
this.selectedValue = "";
this.$refs.buttonQuestion.resetField();
},
scrollToBottom() {
const container = document.getElementsByClassName("page-container-grouped-styles")[0];
container.scrollTo({ top: container.scrollHeight, left: 0, behavior: "smooth" });
},
async reloadShopData(serviceZipCode) {
const result = await this.loadInitialData(serviceZipCode);
this.initializeComponent(result.data);
this.resetShopList();
},
},
watch: {
serviceZipCode: {
async handler(newValue) {
await this.reloadShopData(newValue);
},
},
selectedAppointmentType: {
async handler(newValue) {
// If the validation has been previously triggered, clear it before displaying the component
this.$refs.buttonQuestion.resetField();
this.resetShopList();
await this.getNextShopsFromList();
await this.$nextTick();
this.scrollToBottom();
},
},
shops: {
handler(newValue) {
this.$refs.buttonQuestion.resetField();
const selectedShopIndex = newValue.findIndex(
(provider) => provider.providerNumber == this.modelValue
);
if (selectedShopIndex >= 3) {
this.getNextShopsFromList(selectedShopIndex + 1);
} else {
this.getNextShopsFromList();
}
},
},
},
components: {
alert,
buttonQuestion,
textLink,
},
};
</script>
<style lang="scss">
.shop-question {
margin-top: 1rem !important;
text-align: center !important;
.button-question {
.question-text {
margin-top: 0.5rem !important;
}
}
}
.drop-off-alert {
.alert-heading {
text-align: left;
font-size: 0.75rem !important;
line-height: 1.25rem !important;
}
margin-top: 0.5rem !important;
padding-left: 1.5rem !important;
padding-right: 0.5rem !important;
}
</style>

View file

@ -980,6 +980,14 @@ export const actions = {
});
},
getProviderLocations(context, { serviceZipCode }) {
return globalMethods.callMockHttpClient({
method: endpoints.GetProviderLocations.method,
//TODO: Remove Mocky Endpoints
endpoint: "https://run.mocky.io/v3/abf2fa63-8287-4e46-b169-f5bec65c8dff",
});
},
getSupportingItems(context) {
const glassPartsArray = context.getters.lineItems.glassParts ?? [];
const carId = context.getters.vehicle.carId;