Consolidated YMMS vehicle page
This commit is contained in:
Sneha 2023-07-13 15:20:25 +05:30
parent 2af32f9704
commit bd950a2ec5
9 changed files with 503 additions and 0 deletions

View file

@ -29,6 +29,10 @@ const errorMessages = {
DATE_REQUIRED: "Please select a date",
PHONE_REQUIRED: "Please enter your phone number",
PHONE_FORMAT: "Phone number must be 10 digits",
YEAR_REQUIRED: "Please select your vehicle year",
MAKE_REQUIRED: "Please select your vehicle make",
MODEL_REQUIRED: "Please select your vehicle model",
STYLE_REQUIRED: "Please select your vehicle style",
};
export { errorMessages };

View file

@ -44,6 +44,7 @@ export default {
isDisabled: Boolean,
isRequired: Boolean,
validationRules: String,
placeHolderText: String,
cmsWidgetName: String,
hasError: Boolean,
},

View file

@ -13,6 +13,7 @@
:isDisabled="isForwardActionDisabled"
@click-event="buttonClick"
data-bs-target="#footerModal"
data-test-id="funnel-footer-main-button"
data-bs-dismiss="modal" />
</div>
<div v-if="!isBackButtonHidden" class="col-auto link-col py-1 text-break">

View file

@ -0,0 +1,56 @@
import vehicleQuestion from "@/layouts/vehicle/vehicle-question/vehicle-question";
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import store from "@/store";
jest.mock(
"@/store",
() => {
return {};
},
{ virtual: true }
);
describe("vehicle-question.vue", () => {
test("Selected value is emitted upon selection.", async () => {
//Arrange
const { wrapper } = setupMocks({ modelValueProp: "value" });
const valueToSelect = "newvalue";
//Act
wrapper.setValue({ selectedValue: valueToSelect });
await wrapper.vm.$nextTick();
//Assert
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([{ selectedValue: "newvalue" }]);
});
});
function setupMocks({ modelValueProp = "", dataFromStoreApi = [] }) {
//Mock store
store.dispatch = jest.fn(() => dataFromStoreApi);
store.getters = { vehicle: { year: 2019, make: "honda", model: "civic", style: "4 Door" } };
const mountOptions = getMountOptions({
store: {
dispatch: store.dispatch,
getters: store.getters,
},
});
//Mock props
const mockMixin = {
methods: {
getCmsContent: jest.fn(),
},
};
mountOptions.propsData = {
modelValue: modelValueProp,
};
mountOptions.mixins = [mockMixin];
const wrapper = shallowMount(vehicleQuestion, mountOptions);
return { wrapper };
}

View file

@ -0,0 +1,62 @@
<template>
<dropdownQuestion
:options="values"
disableAutoFill
v-model="selectedValue"
:isDisabled="!values.length" />
</template>
<script>
import dropdownQuestion from "@/digital-components/dropdown-question/dropdown-question";
export default {
name: "vehicle-question",
data() {
return {
values: [],
selectedIndex: null,
};
},
props: {
modelValue: String,
updateValues: Function,
},
components: {
dropdownQuestion,
},
computed: {
selectedValue: {
get() {
return this.selectedIndex?.toString();
},
set(newValue) {
this.selectedIndex = newValue;
newValue = newValue != null && newValue > -1 ? this.values[newValue] : null;
this.$emit("update:modelValue", newValue);
},
},
},
methods: {
async getNewValues(selectedYMMS) {
const results = await this.updateValues();
this.values = results?.data;
if (this.values.length == 1) {
this.selectedValue = 0;
} else {
this.selectedValue =
selectedYMMS != null ? this.values.indexOf(selectedYMMS) : null;
}
},
clearValues() {
this.values = [];
this.selectedValue = null;
},
},
};
</script>

View file

@ -0,0 +1,91 @@
// Components
import vehicle from "@/layouts/vehicle/vehicle.vue";
// Supporting files
import { nextTick } from "vue";
import { mount } from "@vue/test-utils";
describe("vehicle.vue", () => {
test('"Continue" button is enabled after YMMS is selected.', async () => {
const { wrapper } = setupMocks();
const continueButton = wrapper.get('[data-test-id="funnel-footer-main-button"]');
expect(continueButton.attributes()["aria-disabled"]).toBe("false");
});
test("arePagePrerequisitesValid should be true ", async () => {
//Arrange
const { wrapper } = setupMocks();
//Act
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
await nextTick();
//Assert
expect(arePagePrerequisitesValid).toBe(true);
});
});
describe("navigation", () => {
test("forwardButtonAction should trigger navigateForward", async () => {
// Arrange
const { wrapper } = setupMocks();
wrapper.vm.navigateForward = jest.fn();
// Act
await wrapper.vm.forwardButtonAction();
//Assert
expect(wrapper.vm.navigateForward).toHaveBeenCalled();
});
});
const FunnelFooterWidgetMockData = {
ForwardButtonText: "Continue",
};
function setupMocks() {
const mockRoute = {
query: {
fmgPage: "vehicle",
},
};
const mockRouter = {
navigate: jest.fn(),
};
const wrapper = mount(vehicle, {
global: {
mixins: [
{
methods: {
getCmsContent: jest.fn((cmsWidgetName, fieldName) => {
if (cmsWidgetName === "FunnelFooterWidget") {
if (fieldName === "ForwardButtonText") {
return FunnelFooterWidgetMockData.ForwardButtonText;
}
}
return "";
}),
getFooterInfoBoxHeight: jest.fn(() => 80),
},
},
],
mocks: {
$route: mockRoute,
$router: mockRouter,
},
stubs: {
FunnelHeader: true,
FunnelSubHeader: true,
VehicleBanner: true,
},
},
});
return { mockRoute, mockRouter, wrapper };
}

View file

@ -0,0 +1,278 @@
<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit" ref="theForm" v-slot="{ meta }">
<div class="page-container-grouped-styles position-relative">
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<div class="select-car">
<div class="container-fluid pb-2">
<div class="row">
<div class="col">
<div class="select-car-form rounded">
<funnelSubHeader
cmsWidgetName="FunnelSubHeaderWidget"
class="siteSubHeader" />
<vehicleQuestion
ref="vehicleYearQuestion"
class="mb-2 mt-4"
v-model="selectedYear"
cmsWidgetName="VehicleYearQuestion"
:updateValues="updateYearValues"
validationRules="year-required"
placeHolderText="Select year"
inputId="yearQuestionField" />
<vehicleQuestion
ref="vehicleMakeQuestion"
class="mb-2 mt-4"
v-model="selectedMake"
cmsWidgetName="VehicleMakeQuestion"
:updateValues="updateMakeValues"
validationRules="make-required"
placeHolderText="Select make"
inputId="makeQuestionField" />
<vehicleQuestion
ref="vehicleModelQuestion"
class="mb-2 mt-4"
v-model="selectedModel"
cmsWidgetName="VehicleModelQuestion"
:updateValues="updateModelValues"
validationRules="model-required"
placeHolderText="Select model"
inputId="modelQuestionField" />
<vehicleQuestion
ref="vehicleStyleQuestion"
class="mb-2 mt-4"
v-model="selectedStyle"
cmsWidgetName="VehicleStyleQuestion"
:updateValues="updateStyleValues"
validationRules="style-required"
placeHolderText="Select style"
inputId="styleQuestionField" />
<vehicleBanner
cmsWidgetName="VehicleBannerWidget"
:displayGenericVehicleImage="displayGeneric"
class="mt-5 mb-3"
ref="banner" />
<funnelFooter
cmsWidgetName="FunnelFooterWidget"
@ForwardClicked="forwardButtonAction"
:isForwardActionDisabled="!meta.valid"
ref="funnelFooter" />
</div>
</div>
</div>
</div>
</div>
</div>
</Form>
</template>
<script>
// Components
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";
import vehicleBanner from "@/fmg-components/vehicle-banner/vehicle-banner";
import vehicleQuestion from "@/layouts/vehicle/vehicle-question/vehicle-question";
// Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { experimentUniverses } from "@/constants/experiments";
import { getDeviceIdValue, getSessionKeyValue } from "@/helpers/heritage-integration/cookie-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { Form, defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
import store from "@/store";
import { storeActions } from "@/constants/store-actions.js";
import baseMixin from "@/mixins/base-mixin.js";
//define validation rules
defineRule("year-required", required(errorMessages.YEAR_REQUIRED));
defineRule("make-required", required(errorMessages.MAKE_REQUIRED));
defineRule("model-required", required(errorMessages.MODEL_REQUIRED));
defineRule("style-required", required(errorMessages.STYLE_REQUIRED));
export default {
name: "vehicle",
data() {
return {
selectedYear: null,
selectedMake: null,
selectedModel: null,
selectedStyle: null,
};
},
props: {
cmsWidgetName: String,
validationRules: String,
},
mounted() {
this.$refs["vehicleYearQuestion"].getNewValues(this.selectedYearfromStore);
},
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
const experimentForLogging = store.getters.applicationUser.experiments.find(
(e) => e.universeName === experimentUniverses.CONCEPT_FUNNEL
);
// If the concept funnel experiment is found, as it should be when coming from safelite.com, then log the experiment exposure.
if (experimentForLogging !== undefined) {
// Log experiment exposure
baseMixin.methods.dispatchStoreAction(
storeActions.LOG_EXPERIMENT_EXPOSURE,
{
userId: getDeviceIdValue(),
sessionKey: getSessionKeyValue(),
pageName: to.query.fmgPage,
experiment: experimentForLogging,
},
false
);
}
// Settle promises and get results
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: cmsContentPromise,
},
];
let resultMap = await settleAllPromises(promiseResultMap);
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
});
},
watch: {
selectedYear(year) {
const parsedYear = parseInt(year);
this.dispatchStoreAction(storeActions.SAVE_VEHICLE_YEAR, parsedYear);
if (year) {
this.$refs["vehicleMakeQuestion"].getNewValues(this.selectedMakefromStore);
} else {
this.$refs["vehicleMakeQuestion"].clearValues();
}
},
selectedMake(make) {
this.dispatchStoreAction(storeActions.SAVE_VEHICLE_MAKE, make, false);
if (make) {
this.$refs["vehicleModelQuestion"].getNewValues(this.selectedModelfromStore);
} else {
this.$refs["vehicleModelQuestion"].clearValues();
}
},
selectedModel(model) {
this.dispatchStoreAction(storeActions.SAVE_VEHICLE_MODEL, model, false);
if (model) {
this.$refs["vehicleStyleQuestion"].getNewValues(this.selectedStylefromStore);
} else {
this.$refs["vehicleStyleQuestion"].clearValues();
}
},
selectedStyle(style) {
this.dispatchStoreAction(storeActions.SAVE_VEHICLE_STYLE, style, false);
this.setVehicle();
},
},
methods: {
setVehicle() {
return this.dispatchStoreAction(this.storeActions.SET_VEHICLE, {
year: this.$store.getters.vehicle.year,
make: this.$store.getters.vehicle.make,
model: this.$store.getters.vehicle.model,
style: this.$store.getters.vehicle.style,
});
},
arePagePrerequisitesValid() {
return true;
},
async forwardButtonAction() {
return this.navigateForward();
},
navigateForward() {
this.$router.navigateWithSaving(this.navigationScenarios.CLICKED_FORWARD, this.$route);
},
async updateYearValues() {
return await baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_YEARS, {});
},
async updateMakeValues() {
return await baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_MAKES, {
year: store.getters.vehicle.year,
});
},
async updateModelValues() {
return await baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_MODELS, {
year: store.getters.vehicle.year,
make: store.getters.vehicle.make,
});
},
async updateStyleValues() {
return baseMixin.methods.dispatchStoreAction(storeActions.GET_VEHICLE_STYLES, {
year: store.getters.vehicle.year,
make: store.getters.vehicle.make,
model: store.getters.vehicle.model,
});
},
},
computed: {
displayGeneric() {
return !this.selectedStyle;
},
selectedYearfromStore() {
return store.getters.vehicle.year;
},
selectedMakefromStore() {
return store.getters.vehicle.make;
},
selectedModelfromStore() {
return store.getters.vehicle.model;
},
selectedStylefromStore() {
return store.getters.vehicle.style;
},
},
components: {
funnelHeader,
funnelFooter,
funnelSubHeader,
vehicleBanner,
Form,
vehicleQuestion,
},
};
</script>
<style lang="scss">
.select-car-form {
margin-left: 0.75rem;
margin-right: 0.75rem;
}
.siteSubHeader {
margin-top: 1.5rem;
}
</style>

View file

@ -3,6 +3,7 @@ const fmgPageValues = {
VEHICLE_MAKE: "vehicle-make",
VEHICLE_MODEL: "vehicle-model",
VEHICLE_STYLE: "vehicle-style",
VEHICLE: "vehicle",
VEHICLE_DAMAGE: "vehicle-damage",
ADDRESS_LOOKUP: "address-lookup",
VIN_LOOKUP: "vin-lookup",

View file

@ -4,6 +4,15 @@ import { navigationScenarios } from "@/router/router-constants/navigation-scenar
// Get store from router/index.js instead of importing it here to get updated values
const routingTable = function (store) {
return [
{
fmgPageValue: fmgPageValues.VEHICLE,
maps: [
{
scenario: navigationScenarios.CLICKED_FORWARD,
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
},
],
},
{
fmgPageValue: fmgPageValues.VEHICLE_YEAR,
maps: [