This commit is contained in:
Kulbhushan Kaushik 2022-12-01 14:59:13 -05:00
parent d572e03827
commit a2eb94015e
4 changed files with 670 additions and 4 deletions

View file

@ -0,0 +1,408 @@
// Components
import vehicleParts from "@/layouts/vehicle-parts/vehicle-parts.vue";
import glassPartQuestion from "@/layouts/vehicle-parts/glass-part-question/glass-part-question";
// Supporting Files
import { settleAllPromises } from "@/helpers/layout-helper.js";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { nextTick } from "vue";
import baseMixin from "@/mixins/base-mixin.js";
import { useMainStore } from "@/store";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
// 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(),
}));
const basePartResponse = {
partsOrQuestions: [
{
glassName: "Stationary",
glassLocation: "Rear",
parts: [
{
partNumber: "DB12209GTYN",
description: "heated glass, solar, 1 hole",
color: "Green Tint",
requiresRecalibration: false,
requiresCapabilityQuestions: false,
childParts: null,
},
{
partNumber: "DB12209YPYNOEM",
description: "heated glass, solar, 1 hole",
color: "Gray Tint Privacy",
requiresRecalibration: false,
requiresCapabilityQuestions: false,
childParts: null,
},
],
partQuestions: null,
},
],
};
describe("vehicle-parts.vue", () => {
test("Set cms content called on load", async (done) => {
//Arrange
useMainStore().pageData = jest.fn();
useMainStore().pageData.mockReturnValue(basePartResponse);
useMainStore().lineItems = { glassParts: null };
const { wrapper, apiPromise } = setupMocks({
mountOptionsMockData: {
router: {
navigate: jest.fn()
},
route: {
query: {
issPage: "vehicle-parts",
},
},
},
});
//Act
vehicleParts.beforeRouteEnter.call(
wrapper.vm,
{ query: { issPage: "vehicle-parts" } },
undefined,
(c) => c(wrapper.vm)
);
wrapper.vm.setCmsContent = jest.fn();
//Assert
apiPromise.finally(() => {
expect(wrapper.vm.setCmsContent).toHaveBeenCalled();
done();
});
});
test("PageData / isRepair populated in Vuex. arePagePrerequisitesValid should be true ", async () => {
//Arrange
useMainStore().pageData = jest.fn();
useMainStore().pageData.mockReturnValue(basePartResponse);
useMainStore().lineItems = { glassParts: null };
const { wrapper } = setupMocks({
mountOptionsMockData: {
router: {
navigate: jest.fn(),
},
route: {
query: {
issPage: "vehicle-parts",
},
},
},
});
//Act
vehicleParts.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "vehicle-parts" } },
undefined,
(c) => c(wrapper.vm)
);
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
await nextTick();
//Assert
expect(arePagePrerequisitesValid).toBe(true);
});
test("Initial data, should populate this.selectedGlassParts", async () => {
//Arrange
useMainStore().pageData = jest.fn();
useMainStore().pageData.mockReturnValue(basePartResponse);
useMainStore().lineItems = { glassParts: [{ partNumber: "DB12209YPYNOEM" }] };
const { wrapper } = setupMocks({
mountOptionsMockData: {
router: {
navigate: jest.fn(),
},
route: {
query: {
issPage: "vehicle-parts",
},
},
},
});
//Act
vehicleParts.beforeRouteEnter.call(
wrapper.vm,
{ query: { issPage: "vehicle-parts" } },
undefined,
(c) => c(wrapper.vm)
);
await nextTick();
//Assert
expect(wrapper.vm.selectedGlassParts).toEqual({
"Rear-Stationary": {
partNumber: "DB12209YPYNOEM",
description: "heated glass, solar, 1 hole",
color: "Gray Tint Privacy",
requiresRecalibration: false,
requiresCapabilityQuestions: false,
childParts: null,
},
});
});
test("User had part questions > navigateBack triggers a router.navigateWithoutSaving change with correct scenario", async () => {
//Arrange
const { wrapper } = setupMocks({
mountOptionsMockData: {
router: {
navigate: jest.fn(),
},
route: {
query: {
issPage: "vehicle-parts",
},
},
},
});
//Act
vehicleParts.beforeRouteEnter.call(
wrapper.vm,
{ query: { issPage: "vehicle-parts" } },
undefined,
(c) => c(wrapper.vm)
);
wrapper.vm.navigateBack();
//Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalledWith(
navigationScenarios.CLICKED_BACK_WITH_PART_QUESTIONS,
wrapper.vm.$route
);
});
test("User did not have part questions > navigateBack triggers a router.navigate change with correct scenario", async () => {
//Arrange
const { wrapper } = setupMocks({
mountOptionsMockData: {
router: {
navigate: jest.fn(),
},
route: {
query: {
issPage: "vehicle-parts",
},
},
},
});
//Act
vehicleParts.beforeRouteEnter.call(
wrapper.vm,
{ query: { issPage: "vehicle-parts" } },
undefined,
(c) => c(wrapper.vm)
);
wrapper.vm.navigateBack();
//Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalledWith(
navigationScenarios.CLICKED_BACK_WITH_VIN_AND_NO_MORE_QUESTIONS,
wrapper.vm.$route
);
});
test("ForwardButtonAction triggers a router.navigate change if there are child part questions", async () => {
//Arrange
useMainStore().pageData = jest.fn();
useMainStore().pageData.mockReturnValue({
partsOrQuestions: [
{
glassName: "Single",
glassLocation: "Windshield",
parts: [
{
partNumber: "FW03861GTYN",
description:
"rain sensor, heated glass, auto dimming mirror, solar, 3rd visor band, condensation sensor",
color: "Green Tint",
requiresRecalibration: false,
requiresCapabilityQuestions: false,
recalibrationType: null,
childParts: null,
childPartQuestions: [
{
questionSequence: 1,
questionText:
"Does the rubber seal around your windshield have a chrome strip running through it?",
answers: [
{
answerResult: "WKT D1106 C",
answerText: "Yes",
nextQuestionSequence: null,
},
{
answerResult: "WKT D1106 B",
answerText: "No",
nextQuestionSequence: null,
},
],
},
],
},
],
partQuestions: null,
},
],
});
useMainStore().lineItems = { glassParts: {} };
const { wrapper } = setupMocks({
mountOptionsMockData: {
router: {
navigate: jest.fn(),
},
route: {
query: {
issPage: "vehicle-parts",
},
},
},
});
wrapper.setData({
selectedGlassParts: { "Rear-Stationary": { partNumber: "FW03861GTYN" } },
});
//Act
vehicleParts.beforeRouteEnter.call(
wrapper.vm,
{ query: { issPage: "vehicle-parts" } },
undefined,
(c) => c(wrapper.vm)
);
await wrapper.vm.forwardButtonAction();
//Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalled();
});
test("ForwardButtonAction triggers a router.navigate change if there are capability questions", async () => {
//Arrange
useMainStore().pageData = jest.fn();
useMainStore().pageData.mockReturnValue({
partsOrQuestions: [
{
glassName: "Single",
glassLocation: "Windshield",
parts: [
{
partNumber: "DB12209GTYN",
description: "heated glass, solar, 1 hole",
color: "Green Tint",
requiresRecalibration: false,
requiresCapabilityQuestions: true,
childParts: null,
},
],
partQuestions: null,
},
],
});
useMainStore().lineItems = { glassParts: {} };
useMainStore().vehicle = { carId: "TEST_CAR_ID" };
const { wrapper } = setupMocks({
mountOptionsMockData: {
router: {
navigate: jest.fn(),
},
route: {
query: {
issPage: "vehicle-parts",
},
},
actionList: [
{
actionName: "getCapabilityQuestions",
data: [],
},
],
},
});
wrapper.setData({
selectedGlassParts: { "Rear-Stationary": { partNumber: "DB12209GTYN" } },
});
//Act
vehicleParts.beforeRouteEnter.call(
wrapper.vm,
{ query: { issPage: "vehicle-parts" } },
undefined,
(c) => c(wrapper.vm)
);
await wrapper.vm.forwardButtonAction();
//Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalled();
});
});
function setupMocks({ pageHeaderWidgetHeaderText = {}, mountOptionsMockData = {} }) {
//Mock api responses
const apiResponses = {
cmsContent: {
SiteSubHeaderWidget: pageHeaderWidgetHeaderText,
VehicleBannerWidget: {
GenericVehicleImage:
"https://digitalisscms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
},
FunnelHeaderWidget: {
LogoImage:
"https://digitalisscms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
},
ColorQuestionWidget: "Please choose your rear window tint color",
FeatureQuestionWidget: "Ok no choose features",
AlertWidget: {
BodyText: "Please choose your tint color",
HeadlineText: "Just a few more steps to go",
},
},
};
const apiPromise = Promise.resolve(apiResponses);
settleAllPromises.mockImplementation(() => apiPromise);
fetchCmsContentForPage.mockImplementation(() => Promise.resolve());
const mountOptions = getMountOptions(mountOptionsMockData);
const wrapper = shallowMount(vehicleParts, mountOptions);
const partQuestionRearWrapper = wrapper.findComponent({ name: "glassPartQuestion" });
partQuestionRearWrapper.vm.initializeComponent = glassPartQuestion.methods.initializeComponent;
wrapper.vm.setCmsContent = baseMixin.methods.setCmsContent;
wrapper.vm.$refs.siteFooter.removeLoader = jest.fn();
return { wrapper, apiPromise };
}

View file

@ -0,0 +1,222 @@
<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm">
<div class="page-container-grouped-styles vehicle-parts">
<siteHeader ref="siteHeader" cmsWidgetName="SiteHeaderWidget" />
<vehicleBanner
ref="vehicleBanner"
cmsWidgetName="VehicleBannerWidget"
:displayGenericVehicleImage="false" />
<siteSubHeader ref="siteSubHeader" cmsWidgetName="SiteSubHeaderWidget" />
<div class="fade-on-route-transition sub-container make-tall">
<div class="prevent-squish my-5">
<div class="row">
<div class="col">
<alert
class="rounded border-0 shadow-sm"
alertClass="alert-warning"
cmsWidgetName="AlertWidget"
:isDismissible="false" />
</div>
</div>
</div>
<div v-for="(item, i) in PartsOrQuestions" :key="i">
<!-- Render horizontal lines if there is multi-glass (aka if i > 0) -->
<hr v-if="i > 0" />
<glassPartQuestion
:ref="`${RefPrefix}-${item.glassLocation}-${item.glassName}`"
v-model="selectedGlassParts[item.glassLocation + '-' + item.glassName]"
:glassLocation="item.glassLocation"
:glassName="item.glassName"
:colorAnswers="item.colorAnswers"
:alreadyPopulatedPartsData="alreadyPopulatedPartsData" />
</div>
<siteFooter
cmsWidgetName="siteFooterWidget"
ref="siteFooter"
:isForwardActionDisabled="isForwardActionDisabled"
@tempButtonClicked="() => handleTempButtonClicked(this)"
@back-click="navigateBack"
@ForwardClicked="forwardButtonAction" />
</div>
</div>
</Form>
</template>
<script>
// Components
import glassPartQuestion from "@/layouts/vehicle-parts/glass-part-question/glass-part-question";
import siteHeader from "@/common-components/site-header/site-header";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import siteSubHeader from "@/common-components/site-sub-header/site-sub-header";
import siteFooter from "@/common-components/site-footer/site-footer";
import alert from "@/ux-components/alert/alert";
// Supporting Files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { issPageValues } from "@/router/router-constants/issPage-values";
import { Form } from "vee-validate";
import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin";
export default {
name: "vehicle-parts",
mixins: [vehicleQuestionsMixin],
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.issPage);
// Settle promises and get results
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: cmsContentPromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap);
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
// Glass Part Question dynamic component
Object.keys(vm.$refs)
.filter((r) => r.includes(vm.RefPrefix) && vm.$refs[r][0] !== undefined)
.forEach((c) =>
vm.$refs[c][0].initializeComponent({
ColorQuestionWidget: resultMap.cmsContent.ColorQuestionWidget,
FeatureQuestionWidget: resultMap.cmsContent.FeatureQuestionWidget,
})
);
});
},
data() {
return {
selectedGlassParts: {},
alertWidgetData: Object,
alreadyPopulatedPartsData: [],
};
},
computed: {
isForwardActionDisabled() {
return (
this.selectedGlassPartNumbers.length !== this.PartsFromApi.partsOrQuestions.length
);
},
selectedGlassPartNumbers() {
// Compile all selected parts from the page.
const numberArray = [];
for (let glassPart of Object.values(this.selectedGlassParts)) {
if (glassPart?.partNumber) {
numberArray.push(glassPart.partNumber);
}
}
return numberArray;
},
PartsOrQuestions() {
const partsData = this.PartsFromApi;
// Map API result data, to vehicle-parts data structure
const mappedData = partsData.partsOrQuestions.map((g) => {
return {
glassName: g.glassName,
glassLocation: g.glassLocation,
colorAnswers: g.parts?.reduce((arr, p) => {
arr.push({
ColorAnswerText: p.color,
FeatureAnswers: [
{
FeatureAnswerText:
p.description === "" ? p.color : p.description,
PartNumber: p.partNumber,
},
],
});
return arr;
}, []),
};
});
return mappedData;
},
PartsFromApi() {
return this.mainStore.pageData(issPageValues.VEHICLE_PARTS);
},
RefPrefix() {
return "partQuestion";
},
},
methods: {
arePagePrerequisitesValid() {
// Check if isRepair is populated and if the pageData we need is here (Parts data)
return (
this.mainStore.damage.isRepair != null &&
this.mainStore.pageData(issPageValues.VEHICLE_PARTS) &&
Object.keys(this.mainStore.pageData(issPageValues.VEHICLE_PARTS)).length !== 0
);
},
async forwardButtonAction() {
const matchedParts = [];
// Match them to the parts from the API.
for (let [key, value] of Object.entries(this.PartsFromApi.partsOrQuestions)) {
for (let [partKey, partValue] of Object.entries(value.parts)) {
const currentPart = this.PartsFromApi.partsOrQuestions[key].parts[partKey];
const isMatched = this.selectedGlassPartNumbers.some(
(p) => p === currentPart.partNumber
);
if (isMatched) {
matchedParts.push({
glassLocation: value.glassLocation,
glassName: value.glassName,
parts: [currentPart],
});
}
}
}
// If no parts could be matched, throw an error (isForwardActionDisabled is based off of matchedParts)
if (this.isForwardActionDisabled) {
this.$refs.funnelFooter.removeLoader();
throw new Error("Could not match any parts to the selected parts");
}
await this.mainStore.resetMoldingAndCapabilityQuestionAnswersIfNeeded(matchedParts);
this.navigateForward(matchedParts, null);
},
LoadInitialPartsData() {
const partsData = this.PartsFromApi;
this.alreadyPopulatedPartsData =
this.mainStore.lineItems.glassParts === null
? []
: this.mainStore.lineItems.glassParts;
partsData.partsOrQuestions.map((g) => {
// If the part is already populated, use the value from the store and populate the v-model.
Object.keys(this.alreadyPopulatedPartsData).forEach((key) => {
const partNumber = this.alreadyPopulatedPartsData[key].partNumber;
g.parts.forEach((p) => {
if (p.partNumber === partNumber) {
this.selectedGlassParts[g.glassLocation + "-" + g.glassName] = p;
}
});
});
});
},
},
mounted() {
this.LoadInitialPartsData();
},
components: {
Form,
glassPartQuestion,
siteHeader,
vehicleBanner,
siteSubHeader,
siteFooter,
alert,
},
};
</script>

View file

@ -1,6 +1,6 @@
import { issPageValues } from "@/router/router-constants/issPage-values";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
import { useMainStore } from "../store";
import { useMainStore } from "@/store";
export default {
methods: {
@ -405,9 +405,7 @@ export default {
// mimic part-questions page data for consistency
for (let partOrQuestion of partsOrQuestions) {
if (this.hasCapabilityQuestions([partOrQuestion])) {
let capabilityQuestionsForGlassLocation = (
await self.mainStore.getCapabilityQuestions(useMainStore().vehicle.carId, partOrQuestion.parts[0].partNumber)
).data;
let capabilityQuestionsForGlassLocation = (await useMainStore().getCapabilityQuestions(useMainStore().vehicle.carId, partOrQuestion.parts[0].partNumber));
capabilityQuestionsForGlassLocation.forEach((question) => {
question.answers = question.answers.map((answer) => {

View file

@ -91,6 +91,7 @@ export const useMainStore = defineStore({
getters: {
vehicle: (state) => state.order.vehicle,
damage: (state) => state.order.damage,
lineItems: (state) => state.order.lineItems,
eventBusItem: (state) => ( eventCategory, eventSubCategory) => {
const matchedEvent = state.applicationUser.eventBus.find(
@ -354,12 +355,38 @@ export const useMainStore = defineStore({
this.applicationUser.pageData[issPageValues.CAPABILITY_QUESTIONS] = null;
},
resetSupportingItemsState() {
this.order.lineItems.supportingItems = null;
},
resetDamageState() {
this.order.damage.isRepair = null;
this.order.damage.numberOfChips = null;
this.order.damage.glassToReplace = null;
},
resetMoldingAndCapabilityQuestionAnswersIfNeeded(matchedParts) {
const partsOrQuestionsDataToCompareWith =
this.pageData(issPageValues.MOLDING_QUESTIONS)?.partsOrQuestions ??
this.pageData(issPageValues.CAPABILITY_QUESTIONS)?.partsOrQuestions ??
[];
const previouslySelectedPartNumbers = getAllPartNumbers(partsOrQuestionsDataToCompareWith);
const currentlySelectedPartNumbers = getAllPartNumbers(matchedParts);
const haveSelectedVehiclePartsChanged =
previouslySelectedPartNumbers !== currentlySelectedPartNumbers;
if (haveSelectedVehiclePartsChanged) {
this.updateGlassParts(null);
this.resetSupportingItemsState();
this.updateMoldingQuestionAnswers(null);
this.updateCapabilityQuestionAnswers(null);
this.updatePageData({ page: issPageValues.MOLDING_QUESTIONS, data: null});
this.updatePageData({ page: issPageValues.CAPABILITY_QUESTIONS, data: null});
}
},
updateVehicleYear(year) {
if(this.order.vehicle.year != year)
{
@ -726,3 +753,14 @@ function convertGlassPieceNamingFromApi(glassArray) {
return glassArray;
}
function getAllPartNumbers(partsOrQuestions) {
return partsOrQuestions[0]?.parts
? [...partsOrQuestions]
.map((glass) => glass.parts)
.flat()
.map((part) => part.partNumber)
.filter((partNumber) => !partNumber.toUpperCase().includes("FEE"))
.sort()
.join(",")
: [];
}