Merge pull request #156 from Safelite/feature/digital/SSR-207

Feature/digital/ssr 207
This commit is contained in:
katiekroell 2023-02-06 16:04:00 -05:00 committed by GitHub
commit 2146c5e865
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 682 additions and 43 deletions

View file

@ -0,0 +1,77 @@
import addressVehiclesQuestion from "@/layouts/address-vehicles/address-vehicles-question/address-vehicles-question.vue";
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
describe("address-vehicles-question.vue", () => {
test("Selected vehicle is emitted upon selection", async () => {
//Arrange
const { wrapper } = setupMocks({ modelValueProp: "2020 Audi A6" });
const vehicleToSelect = "2016 Jaguar F-Type";
//Act
wrapper.setValue({ selectedVehicle: vehicleToSelect });
await wrapper.vm.$nextTick();
//Assert
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([
{ selectedVehicle: "2016 Jaguar F-Type" },
]);
});
test("Should return content for differentVehicleAlertHeader", () => {
// Arrange
const wrapper = shallowMount(addressVehiclesQuestion, {
mixins: [mockMixin],
});
// Assert
expect(wrapper.vm.differentVehicleAlertHeader).toEqual("FoundWindshieldTestReturn");
});
test("Should return content for differentVehicleAlertBody", () => {
// Arrange
const wrapper = shallowMount(addressVehiclesQuestion, {
mixins: [mockMixin],
propsData: {
vehicles: ["1", "2"],
modelValue: ["1", "2"],
},
});
// Assert
expect(wrapper.vm.differentVehicleAlertBody).toEqual("FoundWindshieldTestReturn");
});
});
const mockMixin = {
methods: {
getCmsContent: jest.fn((contentName) => {
if (contentName === "FoundWindshield") {
return "FoundWindshieldTestReturn";
}
return null;
}),
vehicles: jest.fn(() => {
return [{ vehicle: "test" }];
}),
},
};
function setupMocks({
modelValueProp = "TESTCAR",
cmsQuestionText = "CMS text goes here",
}) {
const mountOptions = getMountOptions();
//Mock props
mountOptions.propsData = {
modelValue: modelValueProp,
};
const wrapper = shallowMount(addressVehiclesQuestion, mountOptions);
//Mock CMS content
const cmsContent = {
QuestionText: cmsQuestionText,
};
return { wrapper, cmsContent };
}

View file

@ -0,0 +1,86 @@
<template>
<alert
ref="differentVehicleAlert"
v-if="isCarIdDifferent"
class="my-4"
cmsWidgetName="FoundWindshield"
:manualHeadline="differentVehicleAlertHeader"
:manualCopy="differentVehicleAlertBody"
alertClass="alert-warning"
v-bind:isDismissible="false"
id="address-vehicles-question-alert" />
<buttonQuestion
class="address-vehicles-question"
ref="addressVehiclesQuestion"
buttonTypeString="listButton"
groupName="ChooseAddressVehicle"
:questionText="questionText"
:answers="vehicles"
v-model="selectedVehicleVin"
isRequired
:validation-rules="validationRules" />
</template>
<script>
import buttonQuestion from "@/digital-components/button-question/button-question";
import alert from "@/ux-components/alert/alert";
// Supporting files
import { getDamageString } from "@/helpers/damage-helper";
export default {
name: "address-vehicles-question",
props: {
vehicles: Array,
modelValue: String,
cmsWidgetName: String,
validationRules: String,
isCarIdDifferent: Boolean,
},
computed: {
differentVehicleAlertHeader() {
return this.getCmsContent("FoundWindshield", "HeadlineText").replaceAll(
"{custom:damage}",
getDamageString()
);
},
differentVehicleAlertBody() {
return this.getCmsContent("FoundWindshield", "BodyText")
.replaceAll("{custom:damage}", getDamageString())
.replaceAll("{custom:vinlookupYear}", this.selectedVehicle?.vehicle.year)
.replaceAll("{custom:vinlookupMake}", this.selectedVehicle?.vehicle.make)
.replaceAll("{custom:vinlookupModel}", this.selectedVehicle?.vehicle.model);
},
questionText() {
return this.getCmsContent("VehicleConfirmationQuestion", "QuestionText");
},
selectedVehicleVin: {
get: function () {
return this.modelValue;
},
set: function (newValue) {
this.$emit("update:modelValue", newValue);
},
},
selectedVehicle() {
// this computed is only needed for the computed differentVehicleAlertBody text above
return this.vehicles.find(({ vin }) => vin === this.selectedVehicleVin);
},
},
components: {
buttonQuestion,
alert,
},
};
</script>
<style lang="scss">
.address-vehicles-question {
.question-text {
margin-bottom: 0.5rem;
& > span {
text-align: left;
}
}
}
#address-vehicles-question-alert p {
margin-bottom: 0 !important; // Overrides extra margin-bottom on alert body text
}
</style>

View file

@ -0,0 +1,252 @@
import addressVehicles from "@/layouts/address-vehicles/address-vehicles";
import { settleAllPromises } from "@/helpers/layout-helper.js";
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { useMainStore } from "@/store";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
jest.mock("@/helpers/damage-helper", () => ({
isGlassAvailableForCarId: jest.fn().mockImplementation(() => true),
getDamageString: jest.fn(),
}));
// Mock fetchCmsContentForPage
jest.mock("@/helpers/cms-content-helper", () => ({
fetchCmsContentForPage: jest.fn(),
doesCopyContainRouterLink: jest.fn(),
splitCopyOnCMSPlaceHolder: jest.fn().mockImplementation(() => "test"),
getRouterLinkRouteFromCopy: jest.fn(),
getRouterLinkDisplayTextFromCopy: jest.fn(),
splitCMSCopyOnParagraphTag: jest.fn(),
}));
// Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({
settleAllPromises: jest.fn(),
}));
describe("address-vehicles.vue", () => {
test("Should navigate to CLICKED_BACK if backButtonAction is run", async () => {
// Arrange
const { wrapper } = setupMocks({});
// Act
await wrapper.vm.backButtonAction();
//Assert
expect(wrapper.vm.$router.navigate).toBeCalled();
});
test("navigateForward should be called if forwardButtonAction is run", async () => {
// Arrange
const { wrapper } = setupMocks({});
const lookupVinResponse = {
data: {
carId: "456"
}
}
wrapper.vm.$refs.siteFooter.updateButtonText = jest.fn();
wrapper.vm.$refs.siteFooter.removeLoader = jest.fn();
wrapper.vm.lookupVin = jest.fn(() => Promise.resolve(lookupVinResponse));
wrapper.vm.$router.navigate = jest.fn();
wrapper.vm.saveVin = jest.fn().mockImplementation(() => {});
wrapper.vm.navigateForward = jest.fn().mockImplementation(() => {});
// Act
await wrapper.setData({
selectedVehicleVin: "5NMS3CADXLH233004",
});
await wrapper.vm.forwardButtonAction();
wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.navigateForward).toHaveBeenCalled();
});
test("Should return out of forwardButtonAction is lookupVin returns an error", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.navigateForwardWithSingleCarMatch = jest.fn();
const lookupVinResponse = {
error: "there is an error",
};
wrapper.vm.$refs.siteFooter.updateButtonText = jest.fn();
wrapper.vm.$refs.siteFooter.removeLoader = jest.fn();
wrapper.vm.lookupVin = jest.fn(() => Promise.reject(lookupVinResponse));
wrapper.vm.$router.navigate = jest.fn();
wrapper.vm.saveVin = jest.fn().mockImplementation(() => {});
// Act
await wrapper.setData({
selectedVehicleVin: "5NMS3CADXLH233004",
});
await wrapper.vm.forwardButtonAction();
wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.forwardButtonAction).toReturn;
});
test("Should navigate to CLICKED_FORWARD scenario if carId is different and selected glass not available for vehicle on navigateForward", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.$refs.siteFooter.updateButtonText = jest.fn();
wrapper.vm.$router.navigate = jest.fn();
// Act
await wrapper.setData({
selectedVehicleVin: "5NMS3CADXLH233004",
isSelectedGlassAvailableForVehicle: false,
isCarIdDifferent: true,
});
await wrapper.vm.navigateForward();
//Assert
expect(wrapper.vm.$router.navigate).toBeCalledTimes(1);
});
test("carId is not different on navigateForward (car was found) => Should handle navigating forward with car match", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.$refs.siteFooter.updateButtonText = jest.fn();
wrapper.vm.navigateForwardWithSingleCarMatch = jest.fn();
// Act
await wrapper.setData({
isCarIdDifferent: false,
});
await wrapper.vm.navigateForward();
//Assert
expect(wrapper.vm.navigateForwardWithSingleCarMatch).toBeCalledTimes(1);
});
test("Should return true for valid page requisites if carId exists", async () => {
// Arrange
const { wrapper } = setupMocks({});
useMainStore().order.vehicle.carId = "CR00000395";
// Act
addressVehicles.beforeRouteEnter.call(
wrapper.vm,
{ query: { issPage: "address-vehicles" } },
undefined,
(c) => c(wrapper.vm)
);
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
// Assert
expect(arePagePrerequisitesValid).toBe(true);
});
});
function setupMocks({
route = null,
lookupVehicleByVinResponse,
})
{
useMainStore().applicationUser = {
pageData: {
"address-vehicles":
[
{
vehicle: {
carId: "CR00069309",
category: "SUV",
imageUrl:
"https://dbhdyzvm8lm25.cloudfront.net/color_0320_032/MY2020/13769/13769_cc0320_032_WW8.jpg",
imageVifColor: "white",
imageVifNumber: "13769",
make: "Hyundai",
model: "Santa Fe",
style: "4 door utility",
year: 2020,
},
vin: "5NMS3CADXLH233004",
},
],
}
};
useMainStore().lookupVehicleByVin = jest.fn().mockImplementation(() => {
return Promise.resolve({
data: lookupVehicleByVinResponse
? lookupVehicleByVinResponse : {
vehicle: {
carId: "CARID"
},
},
})
});
const mountOptions = getMountOptions({
route: route ? route : undefined,
router: {
navigate: jest.fn(),
},
mainStore: {
order: {
vehicle: {
carId: "CR00069309",
category: "SUV",
imageUrl:
"https://dbhdyzvm8lm25.cloudfront.net/color_0320_032/MY2020/13769/13769_cc0320_032_WW8.jpg",
imageVifColor: "white",
imageVifNumber: "13769",
make: "Hyundai",
model: "Santa Fe",
style: "4 door utility",
year: 2020,
},
vin: "5NMS3CADXLH233004",
},
},
},
);
const apiResponses = {
cmsContent: {},
};
settleAllPromises.mockImplementation(() => apiResponses);
fetchCmsContentForPage.mockImplementation(() => Promise.resolve());
//Mock props
const mockMixin = {
methods: {
getCmsContent: jest.fn((contentName) => {
if (contentName === "FoundMultipleVehicles") {
return "FoundMultipleVehiclesTestReturn";
}
if (contentName === "ProvideVinAlert") {
return "ProvideVinAlertTestReturn";
}
return null;
}),
},
computed: {
dynamicStrings() {
return { ROUTER_LINK: "routerLink:" };
},
},
};
mountOptions.mixins = [mockMixin];
const wrapper = shallowMount(addressVehicles, mountOptions);
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => "");
wrapper.vm.setCmsContent = jest.fn();
wrapper.vm.$refs.siteFooter.updateButtonText = jest.fn();
wrapper.vm.$refs.siteFooter.removeLoader = jest.fn();
return { wrapper };
}

View file

@ -2,66 +2,103 @@
<Form
@submit="onSubmit"
@invalidSubmit="onInvalidSubmit"
ref="theForm"
v-slot="{ meta }"
>
<div class="page-container-grouped-styles overflow-auto">
<siteHeader
cmsWidgetName="SiteHeaderWidget"
/>
<siteHeader cmsWidgetName="SiteHeaderWidget" />
<vehicleBanner
cmsWidgetName="VehicleBannerWidget"
:displayGenericVehicleImage="false"
/>
<siteSubHeader
cmsWidgetName="SiteSubHeaderWidget"
<siteSubHeader cmsWidgetName="SiteSubHeaderWidget" />
<alert
cmsWidgetName="FoundMultipleVehicles"
ref="alertFoundMultipleVehicles"
class="my-5"
alertClass="alert-warning"
:manualHeadline="AlertFoundMultipleVehiclesHeader"
manualCopy=""
v-bind:isDismissible="false"
id="multiple-vehicles-alert"
/>
<div class="fade-on-route-transition sub-container make-tall mt-5">
<p>Placeholder for address-vehicles page</p>
<siteFooter
cmsWidgetName="SiteFooterWidget"
:isForwardActionDisabled="!meta.valid"
@backClicked="backButtonAction"
@forwardClicked="forwardButtonAction"
ref="siteFooter"
/>
<addressVehiclesQuestion
ref="addressVehiclesQuestion"
cmsWidgetName="VehicleConfirmationQuestion"
:vehicles="VehiclesForQuestions"
validationRules="vehicle-required"
v-model="selectedVehicleVin"
:isCarIdDifferent="isCarIdDifferent"
/>
<div
class="alert-provide-vin my-3"
v-if="splitAlertProvideVinBodyForLink.length"
>
<span v-for="copy in splitAlertProvideVinBodyForLink" :key="copy">
<span v-if="doesCopyContainRouterLink(copy)" class="text-body">
<router-link
:to="{
query: { issPage: `${getRouterLinkRouteFromCopy(copy)}` },
name: 'root',
}"
>{{ getRouterLinkDisplayTextFromCopy(copy) }}</router-link
>
</span>
<span v-else class="m-0 text-body" v-html="copy"></span>
</span>
</div>
<siteFooter
cmsWidgetName="SiteFooterWidget"
:isForwardActionDisabled="!meta.valid"
@backClicked="backButtonAction"
@forwardClicked="forwardButtonAction"
ref="siteFooter"
/>
</div>
</Form>
</template>
<script>
// Import Supporting Files
import { fetchCmsContentForPage } from '@/helpers/cms-content-helper';
import { settleAllPromises } from '@/helpers/layout-helper';
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { useMainStore } from "@/store";
import { issPageValues } from "@/router/router-constants/issPage-values";
import { errorMessages } from "@/constants/error-messages";
import { required } from "@/helpers/validation-rules";
import { Form, defineRule } from "vee-validate";
import { isGlassAvailableForCarId } from "@/helpers/damage-helper";
import {
doesCopyContainRouterLink,
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy,
} from "@/helpers/cms-content-helper";
import { routerParams } from "@/router/router-constants/router-params";
import vinPagesMixin from "@/mixins/vin-pages-mixin";
// Import Component
import baseFormMixin from '@/mixins/base-form-mixin';
import { Form } 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 vehicleBanner from '@/iss-components/vehicle-banner/vehicle-banner.vue';
import baseFormMixin from "@/mixins/base-form-mixin";
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 vehicleBanner from "@/iss-components/vehicle-banner/vehicle-banner.vue";
import alert from "@/ux-components/alert/alert";
import addressVehiclesQuestion from "@/layouts/address-vehicles/address-vehicles-question/address-vehicles-question";
// DEFINE VALIDATION RULES
defineRule("vehicle-required", required(errorMessages.VEHICLE_REQUIRED));
export default {
name: 'capability-questions',
mixins: [baseFormMixin],
components: {
siteFooter,
siteHeader,
siteSubHeader,
Form,
vehicleBanner,
},
data() {
return {};
},
name: "address-vehicles",
mixins: [baseFormMixin, vinPagesMixin],
async beforeRouteEnter(to, from, next) {
const cmsContentPromise = fetchCmsContentForPage(to.query.issPage);
// Settle promises and get results
const promiseResultMap = [
{
resultKey: 'cmsContent',
resultKey: "cmsContent",
promise: cmsContentPromise,
},
];
@ -72,18 +109,159 @@ export default {
vm.setCmsContent(resultMap.cmsContent);
});
},
props: {
validationRules: String,
},
setup() {
const mainStore = useMainStore();
return { mainStore };
},
data() {
return {
selectedVehicleVin: "",
isCarIdDifferent: false,
isSelectedGlassAvailableForVehicle: true,
};
},
computed: {
vehicleCount() {
return this.VehiclesForQuestions.length;
},
AlertFoundMultipleVehiclesHeader() {
return this.getCmsContent(
"FoundMultipleVehicles",
"HeadlineText"
).replaceAll("{custom:vehicleCount}", this.vehicleCount);
},
AlertProvideVinBody() {
return this.getCmsContent("ProvideVinAlert", "BodyText");
},
splitAlertProvideVinBodyForLink() {
// Splits content when brackets are found in text so that text can be looped through and router-link can be injected when needed
return this.splitCopyOnCMSPlaceHolder(this.AlertProvideVinBody);
},
VehiclesForQuestions() {
// Map API result data, to address-vehicles data structure
const mappedData = this.VehiclesFromApi.map((v) => {
const maskSymbol = "X";
const vinStart = maskSymbol.repeat(v.vin.length - 6);
const vinEnd = v.vin.substring(v.vin.length - 6);
return {
vin: v.vin,
vehicle: v.vehicle,
Text: v.vehicle.year + " " + v.vehicle.make + " " + v.vehicle.model,
Name: v.vin,
SubText: "VIN " + vinStart + vinEnd,
};
});
return mappedData;
},
VehiclesFromApi() {
return useMainStore().pageData(issPageValues.ADDRESS_VEHICLES);
},
selectedVehicle() {
return this.VehiclesForQuestions.find(
({ vin }) => vin === this.selectedVehicleVin
);
},
},
methods: {
arePagePrerequisiteValid() {
return true;
doesCopyContainRouterLink,
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy,
arePagePrerequisitesValid() {
if (this.mainStore.order.vehicle.carId) {
return true;
}
return false;
},
backButtonAction() {
/**
* this.navigationScenarios comes from base-mixin
*/
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
},
async forwardButtonAction() {},
resetDependentState() {},
async forwardButtonAction() {
const vinLookup = await useMainStore()
.lookupVehicleByVin(this.selectedVehicle.vin)
.catch(() => {
this.$refs.siteFooter.removeLoader();
});
if (!vinLookup) {
return;
}
this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId(
vinLookup.data.carId
);
await useMainStore().saveVin(
{
vehicleInfo: Object.assign(this.selectedVehicle.vehicle, {
vin: this.selectedVehicle.vin,
}),
isSelectedGlassAvailableForVehicle:
this.isSelectedGlassAvailableForVehicle,
},
false
);
return await this.navigateForward();
},
async navigateForward() {
// If the vehicle selected on this page is different from the one originally entered and the selected glass is not available
// for that vehicle, then navigate back to "vehicle-damage" and display vehicle changed alert on that page.
if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) {
this.$router.navigate(
this.navigationScenarios.SELECTED_VIN_WITH_MISMATCHED_GLASS,
this.$route,
{},
{ [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: true }
);
} else {
await this.navigateForwardWithSingleCarMatch();
}
},
},
watch: {
selectedVehicleVin: {
handler() {
// does this vehicle match the previously selected carId?
this.isCarIdDifferent =
this.selectedVehicle?.vehicle.carId !== useMainStore().vehicle.carId;
if (this.isCarIdDifferent) {
this.$refs.siteFooter.updateButtonText(
`Continue with ${this.selectedVehicle.vehicle.year} ${this.selectedVehicle.vehicle.make} ${this.selectedVehicle.vehicle.model}`
);
} else {
this.$refs.siteFooter.updateButtonText(
this.getCmsContent("SiteFooterWidget", "ForwardButtonText")
);
}
},
deep: true,
},
},
components: {
siteFooter,
siteHeader,
siteSubHeader,
Form,
vehicleBanner,
alert,
addressVehiclesQuestion,
},
};
</script>
<style lang="scss">
.alert-provide-vin {
font-size: 0.875rem;
line-height: 1.4;
a {
text-underline-offset: 4px; //Per Devyn. This can't be documented in Figma so there is a comment with the Prototype mocks on the Quote page in Figma
line-height: inherit;
}
}
#multiple-vehicles-alert p {
margin-bottom: 0 !important; // Overrides extra margin-bottom on alert body text
}
</style>

View file

@ -321,6 +321,39 @@ const routingTable = function(store) {
},
],
},
{
issPageValue: issPageValues.ADDRESS_VEHICLES,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationIssPageValue: issPageValues.ADDRESS_LOOKUP,
},
{
scenario: navigationScenarios.SELECTED_VIN_WITH_MISMATCHED_GLASS,
destinationIssPageValue: issPageValues.VEHICLE_DAMAGE,
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_PART_QUESTIONS,
destinationIssPageValue: issPageValues.PART_QUESTIONS,
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_MULTIPLE_PARTS_TO_CHOOSE,
destinationIssPageValue: issPageValues.VEHICLE_PARTS,
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_MOLDING_QUESTIONS,
destinationIssPageValue: issPageValues.MOLDING_QUESTIONS,
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_CAPABILITY_QUESTIONS,
destinationIssPageValue: issPageValues.CAPABILITY_QUESTIONS,
},
{
scenario: navigationScenarios.CLICKED_FORWARD_WITH_NO_MORE_QUESTIONS,
destinationIssPageValue: issPageValues.COVERAGE_STATEMENT,
},
],
},
{
issPageValue: issPageValues.WELCOME_PAGE,
maps: [

View file

@ -893,7 +893,20 @@ export const useMainStore = defineStore({
this.updateRegistration(registrationInfo);
}
},
saveVin({ isSelectedGlassAvailableForVehicle, vehicleInfo }) {
//Reset dependent state when changing
if (vehicleInfo.vin !== this.order.vehicle.vin) {
this.resetRegistrationAndDependencies();
if (!isSelectedGlassAvailableForVehicle) {
this.resetDamageAndDependencies();
this.resetPartsAndDependencies();
}
//Save new values
this.updateVehicle(vehicleInfo);
}
},
saveRegistrationLicensePlateLookup({ isSelectedGlassAvailableForVehicle, vehicleInfo, registrationInfo }) {
//Reset dependent state when changing
if