Merging from develop

This commit is contained in:
Leah Schumann 2022-06-10 15:21:27 -04:00
commit b1328b7cdc
24 changed files with 2294 additions and 255 deletions

View file

@ -18,12 +18,10 @@ module.exports = {
"!src/layouts/vehicle-damage/windshield-options/windshield-options.vue",
"!src/layouts/part-questions/**/*.vue",
"!src/layouts/reveal/**/*.vue",
"!src/layouts/estimate/**/*.vue",
"!src/layouts/address-lookup/customer-questions/address-questions/address-questions.vue", // there are limitations of async script loading and third party scripts that prevent full coverage from being met for this component.
"!src/layouts/estimate/**/*.vue",
// TODO REMOVE THESE AFTER WRITING UNIT TESTS
"!src/layouts/address-vehicles/address-vehicles.vue",
"!src/layouts/address-vehicles/address-vehicles-question/address-vehicles-question.vue",
"!src/ux-components/alert\alert.vue",
"!src/ux-components/alert/alert.vue",
"!src/helpers/validation-rules.js",
// END
], // ! means exclude from coverage.

View file

@ -6,6 +6,7 @@
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit --coverage --ci",
"test:unit:lite": "vue-cli-service test:unit --ci",
"lint": "vue-cli-service lint"
},
"dependencies": {

View file

@ -80,7 +80,7 @@ export default {
.bar3 {
width: 14px;
height: 2px;
background-color: $blue-400;
background-color: $blue;
margin: 1px 0;
transition: 0.25s;
}

View file

@ -77,7 +77,7 @@ export default {
left: 0;
right: 0;
bottom: 0;
z-index: 1055;
z-index: 1057;
width: 100%;
height: 100%;
overflow-x: hidden;

View file

@ -55,9 +55,11 @@ export function getMountOptions(mockData) {
mocks.$store = mockData.store;
mocks.$router = mockData.router;
mocks.$route = mockData.route;
mocks.$loadScript = mockData.loadScript;
const global = {
mocks: mocks,
mixins: mockData.mixins,
stubs: { Form }
};

View file

@ -48,9 +48,9 @@ describe("address-lookup.vue", () => {
await wrapper.vm.forwardButtonAction();
// Assert
expect(wrapper.findComponent({ ref: "alertNonServiceableZip" }).isVisible()).toBe(true);
});
expect(wrapper.findComponent({ ref: "alertNonServiceableZip" }).isVisible()).toBe(true);
});
test("if the address matches a different vehicle display the Matched Different VehicleAlert", async () => {
// Arrange
const mockRegistrationAddress = {
@ -76,8 +76,8 @@ describe("address-lookup.vue", () => {
await wrapper.vm.forwardButtonAction();
// Assert
expect(wrapper.findComponent({ ref: "alertMatchedDifferentVehicle" }).isVisible()).toBe(true);
});
expect(wrapper.findComponent({ ref: "alertMatchedDifferentVehicle" }).isVisible()).toBe(true);
});
test("if the looking up VIN by address is not allowed in the state selected display the Vin Lookup By HomeAddress Not Allowed Alert", async () => {
// Arrange
@ -121,7 +121,7 @@ describe("address-lookup.vue", () => {
}
return Promise.resolve({ data });
})
})
await wrapper.setData({
customerQuestions: {
@ -133,7 +133,7 @@ describe("address-lookup.vue", () => {
await wrapper.vm.forwardButtonAction();
// Assert
expect(wrapper.findComponent({ ref: "alertVinLookupsByHomeAddressNotAllowed" }).isVisible()).toBe(true);
expect(wrapper.findComponent({ ref: "alertVinLookupsByHomeAddressNotAllowed" }).isVisible()).toBe(true);
});
@ -169,7 +169,7 @@ describe("address-lookup.vue", () => {
return Promise.resolve({ data });
})
})
await wrapper.setData({
customerQuestions: {
@ -183,13 +183,13 @@ describe("address-lookup.vue", () => {
await wrapper.vm.forwardButtonAction();
// Assert
expect(wrapper.findComponent({ ref: "alertVinNotFound" }).isVisible()).toBe(true);
expect(wrapper.findComponent({ ref: "alertVinNotFound" }).isVisible()).toBe(true);
});
});
});
describe("navigation", () => {
test("if the back button is clicked, navigate back", async () => {
// Arrange
const { wrapper } = setupMocks(addressLookup, {
@ -197,7 +197,7 @@ describe("address-lookup.vue", () => {
});
// Act
await wrapper.vm.backButtonAction();
await wrapper.vm.backButtonAction();
// Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalled();
@ -246,7 +246,7 @@ describe("address-lookup.vue", () => {
}
return Promise.resolve({ data });
})
})
await wrapper.setData({
customerQuestions: {
@ -261,7 +261,7 @@ describe("address-lookup.vue", () => {
// Assert
expect(wrapper.vm.navigateForward).toHaveBeenCalled();
});
});
test("if the car entered matches one of multiple vehicles found, update vehicle info and navigate to the heritage funnel", async () => {
// Arrange
@ -306,7 +306,7 @@ describe("address-lookup.vue", () => {
return Promise.resolve({ data });
})
})
await wrapper.setData({
customerQuestions: {
@ -322,8 +322,7 @@ describe("address-lookup.vue", () => {
// Assert
expect(wrapper.vm.updateVehicleInfo).toHaveBeenCalled();
expect(navigateAfterSaveToHeritageFunnel).toHaveBeenCalled();
});
});
test("if the car entered does not match any of the multiple vehicles found, navigate to address-vehicles page", async () => {
// Arrange
@ -368,20 +367,20 @@ describe("address-lookup.vue", () => {
return Promise.resolve({ data });
})
})
const carsFound = [{
vin: "TEST_VIN",
vehicle: {
carId: "CARID"
}
},
{
vin: "TEST_VIN2",
vehicle: {
carId: "CARID2"
}
}]
vin: "TEST_VIN",
vehicle: {
carId: "CARID"
}
},
{
vin: "TEST_VIN2",
vehicle: {
carId: "CARID2"
}
}]
await wrapper.setData({
customerQuestions: {
@ -396,8 +395,7 @@ describe("address-lookup.vue", () => {
// Assert
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledWith(navigationScenarios.CONTINUING_WITH_MULTIPLE_VEHICLES, undefined, {}, {}, carsFound);
});
});
test("if the car entered matches one of the vehicles found but the zip is NOT serviceable, do not navigate forward", async () => {
// Arrange
@ -442,7 +440,7 @@ describe("address-lookup.vue", () => {
return Promise.resolve({ data });
})
})
await wrapper.setData({
customerQuestions: {
@ -458,7 +456,7 @@ describe("address-lookup.vue", () => {
// Assert
expect(wrapper.vm.navigateForward).toHaveBeenCalledTimes(0);
});
});
test("if a different vehicle is found than the one entered and the selected glass is not available for that vehicle, navigate back to vehicle-damage page", async () => {
// Arrange
@ -492,7 +490,7 @@ describe("address-lookup.vue", () => {
return Promise.resolve({ data });
})
})
await wrapper.setData({
customerQuestions: {
@ -520,13 +518,74 @@ describe("address-lookup.vue", () => {
await wrapper.vm.navigateForward(carEntered, carsFound);
// Assert
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledWith(navigationScenarios.CONTINUING_WITH_DIFFERENT_GLASS, undefined, {}, {"displayVehicleChangeAlert": true}, {});
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledWith(navigationScenarios.SELECTED_VIN_HAS_MISMATCHED_GLASS, undefined, {}, { "displayVehicleChangeAlert": true }, {});
});
test("single car was found and matches entered vehicle => navigateForwardWithSingleCarMatch", async () => {
// Arrange
const carEntered = {
carId: "CARID2"
};
const carsFound = [
{
vin: "TEST_VIN_2",
vehicle: {
carId: "CARID2"
}
}
];
const { wrapper } = setupMocks({}, {});
wrapper.vm.navigateForwardWithSingleCarMatch = jest.fn();
// Act
wrapper.vm.navigateForward(carEntered, carsFound);
// Assert
expect(wrapper.vm.navigateForwardWithSingleCarMatch).toHaveBeenCalledTimes(1);
});
test("multiple cars were found and one matches entered vehicle => navigateForwardWithSingleCarMatch", async () => {
// Arrange
const carEntered = {
carId: "CARID2"
};
const carsFound = [
{
vin: "TEST_VIN_1",
vehicle: {
carId: "CARID1"
}
},
{
vin: "TEST_VIN_2",
vehicle: {
carId: "CARID2"
}
},
{
vin: "TEST_VIN_3",
vehicle: {
carId: "CARID3"
}
}
];
const { wrapper } = setupMocks({}, {});
wrapper.vm.navigateForwardWithSingleCarMatch = jest.fn();
// Act
wrapper.vm.navigateForward(carEntered, carsFound);
// Assert
expect(wrapper.vm.navigateForwardWithSingleCarMatch).toHaveBeenCalledTimes(1);
});
});
describe("reseting dependent state", () => {
describe("resetting dependent state", () => {
test("when reseting dependent state, license plate is set to null and parts state and dependencies are reset", async () => {
// Arrange
const commitSpy = jest.spyOn(store, "commit");
@ -544,7 +603,7 @@ describe("address-lookup.vue", () => {
});
});
describe("registration and service zips", () => {
describe("if registration zip is serviceable", () => {
test("if registration address is provided => update service address on successful continue", async () => {
@ -556,7 +615,7 @@ describe("address-lookup.vue", () => {
zipCode: "43215"
}
const { wrapper } = setupMocks(addressLookup,{
const { wrapper } = setupMocks(addressLookup, {
isZipServiceable: true
});
@ -572,7 +631,7 @@ describe("address-lookup.vue", () => {
await wrapper.vm.forwardButtonAction();
// Assert
expect(baseMixin.methods.dispatchStoreAction).toHaveBeenCalledWith(storeActions.UPDATE_SERVICE_LOCATION_WITH_VEHICLE_REGISTRATION);
expect(wrapper.vm.dispatchStoreAction).toHaveBeenCalledWith(storeActions.UPDATE_SERVICE_LOCATION_WITH_VEHICLE_REGISTRATION);
});
});
@ -619,8 +678,8 @@ describe("address-lookup.vue", () => {
}
const { wrapper } = setupMocks(addressLookup, {
isZipServiceable: false
}
isZipServiceable: false
}
);
expect(wrapper.vm.showServiceZipField).toBeFalsy();
@ -651,8 +710,8 @@ describe("address-lookup.vue", () => {
}
const { wrapper } = setupMocks(addressLookup, {
isZipServiceable: false
}
isZipServiceable: false
}
);
store.commit(storeMutations.UPDATE_CAR_ID, "CARID");
@ -735,7 +794,7 @@ describe("address-lookup.vue", () => {
});
});
function setupMocks(mountOptions, { isZipServiceable = true, lookupVinbyAddressResponse }) {
function setupMocks(mountOptions, { isZipServiceable = true, lookupVinbyAddressResponse, partsOrQuestions = [] }) {
store.commit(storeMutations.RESET_STATE);
const wrapper = shallowMount(addressLookup, getMountOptions({
...mountOptions,
@ -757,7 +816,13 @@ function setupMocks(mountOptions, { isZipServiceable = true, lookupVinbyAddressR
}
}]
}
}
},
{
actionName: storeActions.GET_PARTS_OR_QUESTIONS,
data: {
partsOrQuestions: partsOrQuestions
}
},
],
router: {
navigate: jest.fn(),
@ -772,6 +837,5 @@ function setupMocks(mountOptions, { isZipServiceable = true, lookupVinbyAddressR
wrapper.vm.$refs.loadingModal.showModal = jest.fn();
return { wrapper };
}

View file

@ -85,12 +85,14 @@ import { storeMutations } from "@/constants/store-mutations";
import baseMixin from "@/mixins/base-mixin";
import { navigateAfterSaveToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { getDamageString, isGlassAvailableForCarId } from "@/helpers/damage-helper";
import vinPagesMixin from "@/mixins/vin-pages-mixin";
defineRule("service-zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED));
defineRule("service-zip-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.SERVICE_ZIP_FORMAT));
export default {
name: "address-lookup",
mixins: [vinPagesMixin],
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
@ -291,26 +293,23 @@ export default {
this.$router.navigateAfterSave(
// then navigate back to "vehicle-damage", and display vehicle changed alert
// on that page
this.navigationScenarios.CONTINUING_WITH_DIFFERENT_GLASS,
this.navigationScenarios.SELECTED_VIN_HAS_MISMATCHED_GLASS,
this.$route, {}, {
displayVehicleChangeAlert: true
}, {}
);
} else {
// otherwise
this.$refs.loadingModal.showModal();
navigateAfterSaveToHeritageFunnel(this.$route);
this.navigateForwardWithSingleCarMatch();
}
} else if (carsFound.length > 1) {
// if multiple cars were found
let matchingCars = carsFound.filter(car => car.vehicle.carId === carEntered.carId);
if (matchingCars.length === 1) {
this.$refs.loadingModal.showModal();
// and one and only of them matches the car id entered
const matchingCar = matchingCars[0];
this.updateVehicleInfo(matchingCar.vin, matchingCar.vehicle);
navigateAfterSaveToHeritageFunnel(this.$route);
this.navigateForwardWithSingleCarMatch();
} else {
// if there are no matches or there are multiple matches, navigate to "address-vehicles" page
this.$router.navigateAfterSave(this.navigationScenarios.CONTINUING_WITH_MULTIPLE_VEHICLES, this.$route, {}, {}, carsFound);
@ -329,8 +328,8 @@ export default {
{
licenseLastName: lastName,
licenseStreetAddress: streetAddress,
licenseZip: zip,
licenseState: state,
licenseZip: zip,
licenseState: state
}, false
);
},
@ -361,9 +360,7 @@ export default {
const serviceLocation = store.getters.order.serviceLocation;
if (!serviceLocation.address && serviceLocation.zipCode && serviceLocation.zipCode == store.getters.vehicle.registration.zipCode) {
baseMixin.methods.dispatchStoreAction(
storeActions.UPDATE_SERVICE_LOCATION_WITH_VEHICLE_REGISTRATION
);
this.dispatchStoreAction(storeActions.UPDATE_SERVICE_LOCATION_WITH_VEHICLE_REGISTRATION);
}
}
},

View file

@ -1,130 +1,630 @@
import { shallowMount } from "@vue/test-utils";
// Components
import addressQuestions from "@/layouts/address-lookup/customer-questions/address-questions/address-questions";
import alert from "@/ux-components/alert/alert";
const addressModel = {
streetAddress: "",
city: "",
state: "",
zipCode: "",
}
// Supporting Files
import { mount, shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { storeActions } from "@/constants/store-actions";
import { storeMutations } from "@/constants/store-mutations";
import store from "@/store";
describe("addressQuestions.vue", () => {
let autocompleteElement;
describe("address-questions.vue", () => {
beforeEach(() => {
// Create the `addressField1` element (autocomplete's input)
autocompleteElement = document.createElement("input")
autocompleteElement.getPlace = jest.fn();
document.getElementById = jest.fn().mockReturnValue(autocompleteElement);
})
it("Should render addressQuestions sub-components (textbox-questions and dropdown-questions)", async () => {
// Arrange
const wrapper = shallowMount(addressQuestions);
describe("initial state", () => {
test("only street address field is shown", () => {
// Arrange
const { wrapper } = setupMocks({});
// Act
const streetAddress = wrapper.findComponent({ ref: 'autocomplete' });
const city = wrapper.findComponent({ ref: 'city' });
const state = wrapper.findComponent({ ref: 'state' });
const zipCode = wrapper.findComponent({ ref: 'zipCode' });
// Assert
const streetAddressField = wrapper.findComponent({ ref: "autocomplete" });
const cityField = wrapper.findComponent({ ref: "city" });
const stateField = wrapper.findComponent({ ref: "state" });
const zipCodeField = wrapper.findComponent({ ref: "zipCode" });
// Assert
expect(streetAddress.exists()).toBe(true);
expect(city.exists()).toBe(true);
expect(state.exists()).toBe(true);
expect(zipCode.exists()).toBe(true);
expect(streetAddressField.exists()).toBe(true);
expect(streetAddressField.isVisible()).toBe(true);
expect(cityField.exists()).toBe(true);
expect(cityField.isVisible()).toBe(false);
expect(stateField.exists()).toBe(true);
expect(stateField.isVisible()).toBe(false);
expect(zipCodeField.exists()).toBe(true);
expect(zipCodeField.isVisible()).toBe(false);
const alerts = wrapper.findAllComponents(alert);
expect(alerts.length).toEqual(0);
})
test("Should render addressQuestions sub-components (textbox-questions and dropdown-questions)", async () => {
// Arrange
const { wrapper } = setupMocks({});
// Act
const streetAddress = wrapper.findComponent({ ref: 'autocomplete' });
const city = wrapper.findComponent({ ref: 'city' });
const state = wrapper.findComponent({ ref: 'state' });
const zipCode = wrapper.findComponent({ ref: 'zipCode' });
// Assert
expect(streetAddress.exists()).toBe(true);
expect(city.exists()).toBe(true);
expect(state.exists()).toBe(true);
expect(zipCode.exists()).toBe(true);
});
it("Should call the watch handler for the address model when the addressModel is changed", async () => {
// Arrange
const wrapper = shallowMount(addressQuestions, {
propsData: {
modelValue: addressModel,
test("Should set this.displayNoMatchWarning to false when it is set to true, if the model if prepopulated", async () => {
// Arrange
const newAddressModel = {
streetAddress: "foo",
city: "foo",
state: "foo",
zipCode: "55555",
};
const wrapper = shallowMount(addressQuestions, {
propsData: {
modelValue: newAddressModel,
},
});
await wrapper.setData({
displayNoMatchWarning: true
})
expect(wrapper.vm.displayNoMatchWarning).toBeTruthy();
// Act
wrapper.vm.$options.watch.addressModel.handler.call(wrapper.vm, wrapper.vm.addressModel);
// Assert
expect(wrapper.vm.displayNoMatchWarning).toBeFalsy();
});
test("Should it set this.showAddressFields to true when the model is prepopulated", async () => {
// Arrange
// Act
const newAddressModel = {
streetAddress: "foo",
city: "foo",
state: "foo",
zipCode: "55555",
};
const wrapper = shallowMount(addressQuestions, {
propsData: {
modelValue: newAddressModel,
},
});
// Act
wrapper.vm.setupAddressLookup();
// Assert
expect(wrapper.vm.showAddressFields).toBe(true);
});
});
describe("happy paths", () => {
test("full street address is passed in => address fields are displayed", async () => {
// Arrange/Act
const { wrapper } = setupMocks({
props: {
modelValue: {
streetAddress: "12345 Test Road",
city: "Tests",
state: "OH",
zipCode: "12312"
}
}
});
await wrapper.vm.$nextTick();
// Assert
const cityField = wrapper.findComponent({ ref: "city" });
const stateField = wrapper.findComponent({ ref: "state" });
const zipField = wrapper.findComponent({ ref: "zipCode" });
expect(cityField.exists()).toBeTruthy();
expect(cityField.isVisible()).toBeTruthy();
expect(stateField.exists()).toBeTruthy();
expect(cityField.isVisible()).toBeTruthy();
expect(zipField.exists()).toBeTruthy();
expect(cityField.isVisible()).toBeTruthy();
});
test("full street address is passed in => don't load Google Autocomplete script", async () => {
// Arrange/Act
const { wrapper } = setupMocks({
props: {
modelValue: {
streetAddress: "12345 Test Road",
city: "Tests",
state: "OH",
zipCode: "12312"
}
}
});
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.$loadScript).not.toHaveBeenCalled();
});
test("address field is focused => disable autocomplete", async () => {
// Arrange
let focusEventCallbackFunction;
autocompleteElement.addEventListener = jest.fn().mockImplementation((eventName, callbackFunction) => {
if (eventName == "focus") {
focusEventCallbackFunction = callbackFunction;
}
});
const { wrapper } = setupMocks({});
await wrapper.vm.$nextTick();
// Act
focusEventCallbackFunction();
await wrapper.vm.$nextTick();
// Assert
expect(autocompleteElement.getAttribute("autocomplete")).toEqual("do-not-autofill");
});
test("street address is entered, user chooses good result from autocomplete results => other fields are filled in", async () => {
// Arrange
const { wrapper } = setupMocks({});
await wrapper.setData({
addressModel: {
streetAddress: "123 Test Street"
}
})
const selectedPlace = {
address_components: [
{
long_name: "1234",
short_name: "1234",
types: ["street_number"]
},
{
long_name: "Test Road",
short_name: "Test Road",
types: ["route"]
},
{
long_name: "East Columbus",
short_name: "Columbus",
types: ["neighborhood", "political"]
},
{
long_name: "Columbus",
short_name: "Columbus",
types: ["locality", "political"]
},
{
long_name: "Franklin County",
short_name: "Franklin County",
types: ["administrative_area_level_2", "political"]
},
{
long_name: "Ohio",
short_name: "OH",
types: ["administrative_area_level_1", "political"]
},
{
long_name: "United States",
short_name: "US",
types: ["country", "political"]
},
{
long_name: "43215",
short_name: "43215",
types: ["postal_code"]
},
]
}
// Act
autocompleteElement.dispatchEvent(new CustomEvent("place_changed", { detail: selectedPlace }));
// Assert
const addressModel = wrapper.vm.addressModel;
expect(addressModel.streetAddress).toEqual("1234 Test Road");
expect(addressModel.city).toEqual("Columbus");
expect(addressModel.state).toEqual("OH");
expect(addressModel.zipCode).toEqual("43215");
})
test("street address is entered, user chooses good result from autocomplete results => alerts are cleared", async () => {
// Arrange
const { wrapper } = setupMocks({});
await wrapper.setData({
addressModel: {
streetAddress: "123 Test Street"
},
displayVerificationWarning: true,
displayNoMatchWarning: true
})
let alerts = wrapper.findAllComponents(alert);
alerts.forEach(alert => expect(alert.isVisible()).toBeTruthy());
const selectedPlace = {
address_components: [
{
long_name: "1234",
short_name: "1234",
types: ["street_number"]
},
{
long_name: "Test Road",
short_name: "Test Road",
types: ["route"]
},
{
long_name: "East Columbus",
short_name: "Columbus",
types: ["neighborhood", "political"]
},
{
long_name: "Columbus",
short_name: "Columbus",
types: ["locality", "political"]
},
{
long_name: "Franklin County",
short_name: "Franklin County",
types: ["administrative_area_level_2", "political"]
},
{
long_name: "Ohio",
short_name: "OH",
types: ["administrative_area_level_1", "political"]
},
{
long_name: "United States",
short_name: "US",
types: ["country", "political"]
},
{
long_name: "43215",
short_name: "43215",
types: ["postal_code"]
},
]
}
// Act
autocompleteElement.dispatchEvent(new CustomEvent("place_changed", { detail: selectedPlace }));
await wrapper.vm.$nextTick();
// Assert
alerts = wrapper.findAllComponents(alert);
alerts.forEach(alert => expect(alert.exists()).toBeFalsy());
});
test("street address is entered, but user clicks away => first result is selected and other fields are filled in", async () => {
// Arrange
let changeEventCallbackFunction;
autocompleteElement.addEventListener = jest.fn().mockImplementation((eventName, callbackFunction) => {
if (eventName == "change") {
changeEventCallbackFunction = callbackFunction;
}
});
const { wrapper } = setupMocks({
querySelectorFunction: function (query) {
if (query == ".pac-container .pac-item") {
let element = document.createElement("div");
element.textContent = "123 Test Street"
return element;
}
},
geocoderResult: {
address_components: [
{
long_name: "1234",
short_name: "1234",
types: ["street_number"]
},
{
long_name: "Test Road",
short_name: "Test Road",
types: ["route"]
},
{
long_name: "East Columbus",
short_name: "Columbus",
types: ["neighborhood", "political"]
},
{
long_name: "Columbus",
short_name: "Columbus",
types: ["locality", "political"]
},
{
long_name: "Franklin County",
short_name: "Franklin County",
types: ["administrative_area_level_2", "political"]
},
{
long_name: "Ohio",
short_name: "OH",
types: ["administrative_area_level_1", "political"]
},
{
long_name: "United States",
short_name: "US",
types: ["country", "political"]
},
{
long_name: "43215",
short_name: "43215",
types: ["postal_code"]
},
]
}
});
let noMatchAlert = wrapper.findComponent({ ref: "alertNoMatchWarning" });
let verificationAlert = wrapper.findComponent({ ref: "alertVerificationWarning" });
expect(noMatchAlert.exists()).toBeFalsy();
expect(verificationAlert.exists()).toBeFalsy();
await wrapper.vm.$nextTick();
// Act
changeEventCallbackFunction();
await wrapper.vm.$nextTick();
// Assert
const addressModel = wrapper.vm.addressModel;
expect(addressModel.streetAddress).toEqual("1234 Test Road");
expect(addressModel.city).toEqual("Columbus");
expect(addressModel.state).toEqual("OH");
expect(addressModel.zipCode).toEqual("43215");
});
});
describe("alerts", () => {
const places = [null, { address_components: null }, undefined, {}];
test.each(places)("selected place/place properties is null => display verification alert", async (place) => {
// Arrange
const { wrapper } = setupMocks({});
await wrapper.setData({
addressModel: {
streetAddress: "123 Test Street"
},
displayVerificationWarning: true,
displayNoMatchWarning: true
})
let alerts = wrapper.findAllComponents(alert);
alerts.forEach(alert => expect(alert.isVisible()).toBeTruthy());
const selectedPlace = place;
// Act
autocompleteElement.dispatchEvent(new CustomEvent("place_changed", { detail: selectedPlace }));
await wrapper.vm.$nextTick();
// Assert
const verificationAlert = wrapper.findComponent({ ref: "alertVerificationWarning" });
expect(verificationAlert.exists()).toBe(true);
expect(verificationAlert.isVisible()).toBe(true);
const noMatchAlert = wrapper.findComponent({ ref: "alertNoMatchWarning" });
expect(noMatchAlert.exists()).toBe(false);
});
test("user enters address that yields no autocomplete results => show noMatch alert", async () => {
// Arrange
let changeEventCallbackFunction;
autocompleteElement.addEventListener = jest.fn().mockImplementation((eventName, callbackFunction) => {
if (eventName == "change") {
changeEventCallbackFunction = callbackFunction;
}
});
const { wrapper } = setupMocks({});
let noMatchAlert = wrapper.findComponent({ ref: "alertNoMatchWarning" });
expect(noMatchAlert.exists()).toBeFalsy();
await wrapper.vm.$nextTick();
// Act
changeEventCallbackFunction();
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayNoMatchWarning).toBeTruthy();
noMatchAlert = wrapper.findComponent({ ref: "alertNoMatchWarning" });
expect(noMatchAlert.exists()).toBeTruthy();
expect(noMatchAlert.isVisible()).toBeTruthy();
});
test("user enters address that yields autocomplete results, but doesn't select => show verification alert", async () => {
// Arrange
let changeEventCallbackFunction;
autocompleteElement.addEventListener = jest.fn().mockImplementation((eventName, callbackFunction) => {
if (eventName == "change") {
changeEventCallbackFunction = callbackFunction;
}
});
const { wrapper } = setupMocks({
querySelectorFunction: function (query) {
if (query == ".pac-container .pac-item") {
let element = document.createElement("div");
element.textContent = "123 Test Street"
return element;
}
}
});
let noMatchAlert = wrapper.findComponent({ ref: "alertNoMatchWarning" });
let verificationAlert = wrapper.findComponent({ ref: "alertVerificationWarning" });
expect(noMatchAlert.exists()).toBeFalsy();
expect(verificationAlert.exists()).toBeFalsy();
await wrapper.vm.$nextTick();
// Act
changeEventCallbackFunction();
await wrapper.vm.$nextTick();
// Assert
verificationAlert = wrapper.findComponent({ ref: "alertVerificationWarning" });
expect(wrapper.vm.displayVerificationWarning).toBeTruthy();
expect(verificationAlert.exists()).toBeTruthy();
expect(verificationAlert.isVisible()).toBeTruthy();
noMatchAlert = wrapper.findComponent({ ref: "alertNoMatchWarning" });
expect(wrapper.vm.displayNoMatchWarning).toBeFalsy();
expect(noMatchAlert.exists()).toBeFalsy();
});
describe("noMatch alert is cleared on address change", () => {
test("user sees noMatch warning and enters city => noMatch warning is removed", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.setData({
displayNoMatchWarning: true
});
wrapper.vm.displayNoMatchWarning = true;
await wrapper.vm.$nextTick();
let noMatchAlert = wrapper.findComponent({ ref: "alertNoMatchWarning" });
expect(noMatchAlert.exists()).toBeTruthy();
expect(noMatchAlert.isVisible()).toBeTruthy();
// Act
const newAddressModel = {
streetAddress: "foo",
city: "foo",
state: "foo",
zipCode: "55555",
};
wrapper.vm.$options.watch.addressModel.handler.call(wrapper.vm, newAddressModel);
wrapper.vm.$options.watch.addressModel.handler.call(wrapper.vm, {
city: "Somewhere"
})
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.addressModel.handler).toHaveBeenCalled;
expect(wrapper.vm.displayNoMatchWarning).toBeFalsy();
noMatchAlert = wrapper.findComponent({ ref: "alertNoMatchWarning" });
expect(noMatchAlert.exists()).toBeFalsy();
});
});
test("user sees noMatch warning and enters state => noMatch warning is removed", async () => {
// Arrange
const { wrapper } = setupMocks({});
it("Should set this.displayNoMatchWarning to false, if the model changes when it is set to true", async () => {
// Arrange
const wrapper = shallowMount(addressQuestions, {
propsData: {
modelValue: addressModel,
},
wrapper.setData({
displayNoMatchWarning: true
});
wrapper.vm.displayNoMatchWarning = true;
// Act
const newAddressModel = {
streetAddress: "foo",
city: "",
state: "",
zipCode: "",
};
await wrapper.vm.$nextTick();
let noMatchAlert = wrapper.findComponent({ ref: "alertNoMatchWarning" });
expect(noMatchAlert.exists()).toBeTruthy();
expect(noMatchAlert.isVisible()).toBeTruthy();
// Act
wrapper.vm.$options.watch.addressModel.handler.call(wrapper.vm, newAddressModel);
wrapper.vm.$options.watch.addressModel.handler.call(wrapper.vm, {
state: "KO"
})
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayNoMatchWarning === false);
expect(wrapper.vm.displayNoMatchWarning).toBeFalsy();
noMatchAlert = wrapper.findComponent({ ref: "alertNoMatchWarning" });
expect(noMatchAlert.exists()).toBeFalsy();
});
});
it("Should it not should set this.displayNoMatchWarning to false when it is set to true, if the model if prepopulated", async () => {
// Arrange
const wrapper = shallowMount(addressQuestions, {
propsData: {
modelValue: addressModel,
},
test("user sees noMatch warning and enters zip code => noMatch warning is removed", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.setData({
displayNoMatchWarning: true
});
wrapper.vm.displayNoMatchWarning = true;
// Act
const newAddressModel = {
streetAddress: "foo",
city: "foo",
state: "foo",
zipCode: "55555",
};
await wrapper.vm.$nextTick();
let noMatchAlert = wrapper.findComponent({ ref: "alertNoMatchWarning" });
expect(noMatchAlert.exists()).toBeTruthy();
expect(noMatchAlert.isVisible()).toBeTruthy();
// Act
wrapper.vm.$options.watch.addressModel.handler.call(wrapper.vm, newAddressModel);
wrapper.vm.$options.watch.addressModel.handler.call(wrapper.vm, {
zipCode: "12345"
})
await wrapper.vm.$nextTick();
// Assert
expect(wrapper.vm.displayNoMatchWarning === true);
expect(wrapper.vm.displayNoMatchWarning).toBeFalsy();
noMatchAlert = wrapper.findComponent({ ref: "alertNoMatchWarning" });
expect(noMatchAlert.exists()).toBeFalsy();
});
});
});
});
});
function setupMocks({ mountOptions, props, isShallowMount = true, querySelectorFunction, geocoderResult = ["1234 Test Street"] }) {
store.commit(storeMutations.RESET_STATE);
it("Should it set this.showAddressFields to true when the model is prepopulated", async () => {
// Arrange
const wrapper = shallowMount(addressQuestions, {
propsData: {
modelValue: addressModel,
},
});
wrapper.vm.displayNoMatchWarning = true;
// Act
const newAddressModel = {
streetAddress: "foo",
city: "foo",
state: "foo",
zipCode: "55555",
};
const resultingMountOptions = getMountOptions({
...mountOptions,
router: {
navigate: jest.fn(),
navigateAfterSave: jest.fn()
},
loadScript: jest.fn().mockResolvedValue()
});
// Act
wrapper.vm.setupAddressLookup();
window.google = {
maps: {
event: {
addListener: jest.fn().mockImplementation((element, eventName, callbackFunction) => {
function interceptedCallbackFunction(e) {
callbackFunction(e.detail);
}
// selectedPlace = "Woogly";
element.addEventListener(eventName, interceptedCallbackFunction);
}),
removeListener: jest.fn(),
clearInstanceListeners: jest.fn()
},
places: {
Autocomplete: jest.fn().mockImplementation((el) => el)
},
Geocoder: class Geocoder {
// constructor();
// Assert
expect(wrapper.vm.showAddressFields).toBe(true);
geocode(request, callback) {
callback([geocoderResult], true)
}
},
GeocoderStatus: {
OK: true
}
}
};
});
if (props)
resultingMountOptions.propsData = props;
})
const wrapper = isShallowMount ? shallowMount(addressQuestions, resultingMountOptions) : mount(addressQuestions, resultingMountOptions);
document.querySelector = jest.fn().mockImplementation(query => {
let result = null;
if (query == ".pac-container")
result = document.createElement("div");
else if (querySelectorFunction) {
result = querySelectorFunction(query);
}
return result ?? null;
});
return { wrapper };
}

View file

@ -185,11 +185,10 @@ export default ({
},
methods: {
setupAddressLookup() {
if (this.addressModel.streetAddress !== null &
this.addressModel.city !== null &
this.addressModel.state !== null &
this.addressModel.zipCode !== null) {
if (this.addressModel.streetAddress &&
this.addressModel.city &&
this.addressModel.state &&
this.addressModel.zipCode) {
this.showAddressFields = true;
return;
@ -228,10 +227,9 @@ export default ({
if (autocompleteResultsContainer) {
streetAddressField.appendChild(autocompleteResultsContainer);
}
})
addressField1.onchange = function() {
addressField1.addEventListener("change", () => {
const hover = document.querySelector(".pac-container .pac-item:hover");
// if an item has been clicked, do nothing, otherwise get first solution and use Geocoder to get the place
if (hover === null) {
@ -258,7 +256,7 @@ export default ({
self.displayNoMatchWarning = true;
}
}
};
});
function fillInAddress(place) {
if (!place) {
@ -310,7 +308,6 @@ export default ({
addressField1.onchange = null;
const pacContainer = document.querySelector(".pac-container");
pacContainer.remove();
}
})
.catch(() => {
@ -325,16 +322,10 @@ export default ({
watch: {
addressModel: {
handler(newValue) {
// The first time the address model changes is when the page first loads
if (!newValue.city &&
!newValue.state &&
!newValue.zipCode) {
return;
}
if (this.displayNoMatchWarning === true) {
// Clear no match warning on address change
if (newValue.city || newValue.state || newValue.zipCode) {
this.displayNoMatchWarning = false;
}
}
},
deep: true
}

View file

@ -7,7 +7,6 @@ import { getMountOptions } from "@/helpers/unit-test-helper.js";
import store from "@/store";
import * as navigateToHeritage from "@/helpers/heritage-integration/navigation-helper";
// Mock our module for promises.
jest.mock("@/helpers/damage-helper", () => ({
isGlassAvailableForCarId: () => {
@ -17,7 +16,6 @@ jest.mock("@/helpers/damage-helper", () => ({
describe("addressVehicles.vue", () => {
test("Should return true for valid page requisites if carId / zipCode / emailAddress / pageData exists", async () => {
// Arrange
const { wrapper } = setupMocks({});
@ -225,40 +223,32 @@ describe("addressVehicles.vue", () => {
wrapper.unmount();
});
test("Should navigate to navigateAfterSaveToHeritageFunnel if carId is not different on navigateForward", async () => {
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.funnelFooter.updateButtonText = jest.fn();
wrapper.vm.$refs.loadingModal.showModal = jest.fn();
navigateToHeritage.navigateAfterSaveToHeritageFunnel = jest.fn();
wrapper.vm.navigateForwardWithSingleCarMatch = jest.fn();
// Act
wrapper.setData({
selectedVehicleVin: ['5NMS3CADXLH233004'],
isSelectedGlassAvailableForVehicle: true,
isCarIdDifferent: false,
});
await wrapper.vm.navigateForward();
//Assert
expect(navigateToHeritage.navigateAfterSaveToHeritageFunnel).toBeCalledTimes(1);
expect(wrapper.vm.navigateForwardWithSingleCarMatch).toBeCalledTimes(1);
wrapper.unmount();
});
});
function setupMocks({
// modelValueProp = "1900",
cmsQuestionText = "CMS text goes here",
// dataFromStoreApi = [],
}) {
function setupMocks({}) {
//Mock store
store.dispatch = jest.fn(() => {});
store.getters = {
pageData: jest.fn((pageName) => {
// console.log('pageName: ', pageName) // address-vehicles
return [
{
vehicle: {
@ -295,9 +285,6 @@ function setupMocks({
}
};
// baseMixin.methods.dispatchStoreAction = jest.fn();
const mountOptions = getMountOptions({
store: {
dispatch: store.dispatch,
@ -320,16 +307,6 @@ function setupMocks({
}
return null;
}),
// dispatchStoreAction: jest.fn(() => {
// console.log("23424243")
// }),
// isGlassAvailableForCarId: () => {
// console.log("%%%%%%%%%%%%%%%")
// return Promise.resolve(true)
// }
// lookupVin: jest.fn(() => Promise.resolve(lookupVinResponse)),
},
computed: {
dynamicStrings() {
@ -337,18 +314,10 @@ function setupMocks({
}
}
}
// mountOptions.propsData = {
// modelValue: modelValueProp,
// };
mountOptions.mixins = [mockMixin];
const wrapper = shallowMount(addressVehicles, mountOptions);
//Mock CMS content
const cmsContent = {
QuestionText: cmsQuestionText,
};
return { wrapper };
}

View file

@ -71,13 +71,15 @@ import { isGlassAvailableForCarId } from "@/helpers/damage-helper";
import { doesCopyContainRouterLink,
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy, } from "@/helpers/cms-content-helper"
getRouterLinkDisplayTextFromCopy, } from "@/helpers/cms-content-helper";
import vinPagesMixin from "@/mixins/vin-pages-mixin";
// DEFINE VALIDATION RULES
defineRule("vehicle-required", required(errorMessages.VEHICLE_REQUIRED));
export default {
name: "address-vehicles",
mixins: [vinPagesMixin],
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
@ -188,8 +190,7 @@ export default {
);
return;
} else {
this.$refs.loadingModal.showModal();
navigateAfterSaveToHeritageFunnel(this.$route);
this.navigateForwardWithSingleCarMatch();
return;
}
},

View file

@ -0,0 +1,187 @@
//Components
import estimate from "@/layouts/estimate/estimate.vue";
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
//Supporting files
import { storeKey } from "vuex";
import store from "@/store";
import { storeMutations } from "@/constants/store-mutations";
import { settleAllPromises } from "@/helpers/layout-helper.js";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import baseMixin from "../../mixins/base-mixin";
import { vinLookupMethodSelections } from "@/constants/vin-lookup-method-selections.js";
// 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(),
}));
describe("estimate.vue", () => {
test("Selected vin option is emitted upon selection.", async () => {
//Arrange
const { wrapper } = setupMocks({ modelValueProp: ["Provide my VIN manually most specific to your vehicle"] });
//Act
wrapper.setValue({ modelValue: ["Provide my license plate # Most accurate VIN match"] });
await wrapper.vm.$nextTick();
//Assesrt
expect(wrapper.emitted()["update:modelValue"][0]).toEqual([{ modelValue: ["Provide my license plate # Most accurate VIN match"] }]);
});
test("isRepair is set to true, arePagePrerequisitesValid should return true", async () => {
//Arrange
const { wrapper } = setupMocks({});
//Act
store.commit( storeMutations.UPDATE_IS_REPAIR, true );
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
//Assert
expect(arePagePrerequisitesValid).toBe(true);
});
test("isRepair is set to false, arePagePrerequisitesValid should return true", async () => {
//Arrange
const { wrapper } = setupMocks({});
//Act
store.commit( storeMutations.UPDATE_IS_REPAIR, false );
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
//Assert
expect(arePagePrerequisitesValid).toBe(true);
});
test("isRepair is set to null, arePagePrerequisitesValid should return false", async () => {
//Arrange
const { wrapper } = setupMocks({});
//Act
store.commit( storeMutations.UPDATE_IS_REPAIR, null );
let arePagePrerequisitesValid = wrapper.vm.arePagePrerequisitesValid();
//Assert
expect(arePagePrerequisitesValid).toBe(false);
});
test("After selecting provide my home address on ForwardButtonAction triggers a router.navigate", async () => {
//Arrange
const { wrapper } = setupMocks({});
await wrapper.setData({
selectedValues: [vinLookupMethodSelections.HOMEADDRESS]
})
//Act
wrapper.vm.forwardButtonAction();
//Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalled();
});
test("After selecting provide my manual vin on ForwardButtonAction triggers a router.navigate", async () => {
//Arrange
const { wrapper } = setupMocks({});
await wrapper.setData({
selectedValues: [vinLookupMethodSelections.MANUALVIN]
})
//Act
wrapper.vm.forwardButtonAction();
//Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalled();
});
test("BackButtonAction triggers a router.navigate change", async () => {
//Arrange
const { wrapper } = setupMocks({});
//Act
estimate.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "estimate" } },
undefined,
(c) => c(wrapper.vm)
);
wrapper.vm.backButtonAction();
//Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalled();
});
test("Provide my license plate on ForwardButtonAction triggers a router.navigate", async () => {
//Arrange
const { wrapper } = setupMocks({});
await wrapper.setData({
selectedValues: [vinLookupMethodSelections.LICENSEPLATE]
})
//Act
wrapper.vm.forwardButtonAction();
//Assert
expect(wrapper.vm.$router.navigate).toHaveBeenCalled();
});
});
function setupMocks({
modelValueProp = ["Provide my VIN manually most specific to your vehicle"],
isMultiSelect = false,
groupName = "estimate",
cmsQuestionText = "Let's get your VIN. Or we can look it up for you!",
cmsAnswers = [{ Name: "Provide my VIN manually Most specific to your vehicle" }, { Name: "Provide my license plate # Most accurate VIN match" }, { Name: "Provide my home address Most convenient VIN match" }],
dataFromApi = [],
mountOptionsMockData = {
router: {
navigate: jest.fn(),
},
},
}) {
//Mock CMS Content
const cmsContent = {
groupName: groupName,
QuestionText: cmsQuestionText,
Answers: cmsAnswers
};
//Mock props
const mockMixin = {
methods: {
getCmsContent: jest.fn()
}
}
const apiPromise = Promise.resolve(cmsContent);
settleAllPromises.mockImplementation(() => apiPromise);
fetchCmsContentForPage.mockImplementation(() => Promise.resolve());
const mountOptions = getMountOptions({ ...mountOptionsMockData, mixins: [baseMixin] });
mountOptions['attachTo'] = document.body;
const wrapper = shallowMount(estimate, mountOptions);
return { wrapper, apiPromise };
}

View file

@ -9,6 +9,7 @@ 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 { storeActions } from "@/constants/store-actions";
import { storeMutations } from "@/constants/store-mutations";
import store from "@/store";
@ -223,7 +224,7 @@ describe("license-plate-lookup.vue", () => {
});
describe("navigateForward", () => {
test("NavigateAfterSave should be called if isCarIdDifferent is true and isSelectedGlassAvailableForVehicle is false when navigateForward is called", async () => {
test("navigateAfterSave should be called if isCarIdDifferent is true and isSelectedGlassAvailableForVehicle is false when navigateForward is called", async () => {
// Arrange
const { wrapper } = setupMocks({});
@ -259,6 +260,38 @@ describe("license-plate-lookup.vue", () => {
//Assert
expect(navigateToHeritage.navigateAfterSaveToHeritageFunnel).toHaveBeenCalled();
});
test("carId matches returned vehicle => navigateForwardWithSingleCarMatch", async () => {
// Arrange
const { wrapper } = setupMocks({});
await wrapper.setData({
isCarIdDifferent: false
})
wrapper.vm.navigateForwardWithSingleCarMatch = jest.fn();
// Act
await wrapper.vm.navigateForward();
//Assert
expect(wrapper.vm.navigateForwardWithSingleCarMatch).toHaveBeenCalledTimes(1);
});
test("selected glass is available for returned vehicle => navigateForwardWithSingleCarMatch", async () => {
// Arrange
const { wrapper } = setupMocks({});
await wrapper.setData({
isSelectedGlassAvailableForVehicle: true
})
wrapper.vm.navigateForwardWithSingleCarMatch = jest.fn();
// Act
await wrapper.vm.navigateForward();
//Assert
expect(wrapper.vm.navigateForwardWithSingleCarMatch).toHaveBeenCalledTimes(1);
});
});
});
@ -484,7 +517,7 @@ describe("license-plate-lookup.vue", () => {
expect(store.dispatch).toHaveBeenCalled();
})
test("dispatch non blocking store action called on validate zip", async () => {
test("dispatchStoreAction called on validate zip", async () => {
// Arrange
const { wrapper } = setupMocks({});
@ -497,7 +530,7 @@ describe("license-plate-lookup.vue", () => {
expect(baseMixin.methods.dispatchStoreAction).toHaveBeenCalled();
});
test("dispatch non blocking store action called on lookup vin", async () => {
test("dispatchStoreAction called on lookup vin", async () => {
// Arrange
const { wrapper } = setupMocks({});
@ -514,11 +547,8 @@ describe("license-plate-lookup.vue", () => {
function setupMocks({
pageHeaderWidgetHeaderText = {},
mountOptionsMockData = {
router: {
navigate: jest.fn(),
},
},
mountOptionsMockData = {},
partsOrQuestions = []
}) {
store.commit(storeMutations.RESET_STATE);
//Mock api responses
@ -537,6 +567,21 @@ function setupMocks({
},
};
mountOptionsMockData = {
...mountOptionsMockData,
router: {
navigate: jest.fn(),
},
actionList: [
{
actionName: storeActions.GET_PARTS_OR_QUESTIONS,
data: {
partsOrQuestions: partsOrQuestions
}
},
]
}
const apiPromise = Promise.resolve(apiResponses);
settleAllPromises.mockImplementation(() => apiPromise);

View file

@ -98,10 +98,10 @@ import baseMixin from "@/mixins/base-mixin.js";
import { storeActions } from "@/constants/store-actions";
import { storeMutations } from "@/constants/store-mutations";
import { errorMessages } from "@/constants/error-messages";
import { navigateAfterSaveToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { getDamageString, isGlassAvailableForCarId, } from "@/helpers/damage-helper";
import { required, regex, } from "@/helpers/validation-rules";
import { Form, defineRule, } from "vee-validate";
import vinPagesMixin from "@/mixins/vin-pages-mixin";
// DEFINE VALIDATION RULES
defineRule("license-plate-required", required(errorMessages.LICENSE_PLATE_REQUIRED));
@ -112,6 +112,7 @@ defineRule("email-address-format", regex(/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9_\-.]+
export default {
name: "license-plate-lookup",
mixins: [vinPagesMixin],
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
@ -297,8 +298,7 @@ export default {
);
return;
} else {
this.$refs.loadingModal.showModal();
navigateAfterSaveToHeritageFunnel(this.$route);
this.navigateForwardWithSingleCarMatch();
return;
}
},
@ -312,7 +312,7 @@ export default {
},
updateCustomerInfo(vin, vehicleInfo, registrationState, serviceState) {
if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) {
store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
store.dispatch(storeActions.RESET_DAMAGE_STATE_AND_DEPENDENCIES);
}
store.commit(storeMutations.UPDATE_VEHICLE_VIN, vin);
store.commit(storeMutations.UPDATE_YEAR, vehicleInfo.year);

View file

@ -5,9 +5,7 @@
<funnelSubHeader cmsWidgetName="FunnelSubHeaderWidget" />
<div class="fade-on-route-transition sub-container make-tall">
<h1>Part Questions Page Placeholder</h1>
<funnel-footer
cmsWidgetName="FunnelFooterWidget" @back-clicked="backButtonAction"
/>
<funnel-footer cmsWidgetName="FunnelFooterWidget" @back-clicked="backButtonAction" />
</div>
</div>
</template>
@ -65,10 +63,7 @@ export default {
);
},
arePagePrerequisitesValid() {
if (Object.keys(store.getters.pageData(fmgPageValues.PART_QUESTIONS)).length !== 0) {
return true;
}
return false;
return Object.keys(store.getters.pageData(fmgPageValues.PART_QUESTIONS)).length !== 0;
},
resetDependentState() {
// Set

View file

@ -168,7 +168,7 @@ export default {
// Check if only a single part is present for the tint and set the v-model if it is.
AutoSelectIfSinglePart() {
// Check if the selected tint only has a single feature
Object.keys(this.PartDataFromApi.partsOrQuestions).forEach((key) => {
Object.keys(this.PartDataFromApi.partsOrQuestions ?? {}).forEach((key) => {
const currentGlassSelection =
this.PartDataFromApi.partsOrQuestions[key];

View file

@ -1 +1,261 @@
test.todo("some test to be written in the future");
import { shallowMount } from "@vue/test-utils";
import vinLookup from "./vin-lookup.vue";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios.js";
import store from "@/store";
jest.mock("@/store", () => ({
commit: jest.fn(),
dispatch: jest.fn(),
getters: {
vehicle: {
year: 2019,
carId: 'initial carId'
},
order: {
serviceLocation: {
zipCode: "45253"
},
customer: {
emailAddress: "builddigitaltest@safelite.com"
}
},
payment: {
insuranceCoverage: {
isVerified: true
}
},
damage: {
glassToReplace: "windshield"
}
},
}));
import { getDamageString, getIsWindshieldOnly, isGlassAvailableForCarId } from "@/helpers/damage-helper";
jest.mock("@/helpers/damage-helper", () => ({
isGlassAvailableForCarId: jest.fn(() => {
return Promise.resolve();
}),
getIsWindshieldOnly: jest.fn(),
getDamageString: jest.fn(),
}));
describe("vin-lookup.vue", () => {
it("Should update the funnel-footer forward button when VIN is changed", (done) => {
//Arrange
const { wrapper } = setupMocks({});
//Act
wrapper.setData({ vin: "newValue" });
//Assert
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.$refs.funnelFooter.updateButtonText).toBeCalled();
done();
});
});
it("Should call navigateForward() if the store carId matches the vin response carId and forward button is clicked", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.navigateForward = jest.fn();
// Act
await wrapper.vm.forwardButtonAction();
//Assert
expect(wrapper.vm.navigateForward).toHaveBeenCalled();
});
it("Should not call navigateForward() if the store carId does not match the vin response carId and forward button is clicked", async () => {
// Arrange
const { wrapper } = setupMocks({});
const vehicleLookupApiResponse = {
data: {
carId: 'new carId' // does not match the store value
}
};
const vinPromise = Promise.resolve(vehicleLookupApiResponse);
wrapper.vm.lookupVehicle = jest.fn().mockImplementation(() => vinPromise);
wrapper.vm.navigateForward = jest.fn();
// Act
await wrapper.vm.forwardButtonAction();
//Assert
expect(wrapper.vm.navigateForward).not.toHaveBeenCalled();
});
it("Should call navigateForward() if the store carId does not match the vin response carId but does match previously enterted carId and forward button is clicked", async () => {
// Arrange
const { wrapper } = setupMocks({});
const vehicleLookupApiResponse = {
data: {
carId: 'new carId' // does not match the store value
}
};
const vinPromise = Promise.resolve(vehicleLookupApiResponse);
wrapper.vm.lookupVehicle = jest.fn().mockImplementation(() => vinPromise);
wrapper.vm.navigateForward = jest.fn();
wrapper.vm.previouslyEnteredCarId = 'new carId';
// Act
await wrapper.vm.forwardButtonAction();
//Assert
expect(wrapper.vm.navigateForward).toHaveBeenCalled();
});
it("Should not call navigateForward() if zip service returns a non-serviceable flag", async () => {
// Arrange
const { wrapper } = setupMocks({});
const zipValidationApiResponse = {
data: {
isServiceable: false
}
};
const zipPromise = Promise.resolve(zipValidationApiResponse);
wrapper.vm.validateZip = jest.fn().mockImplementation(() => zipPromise);
wrapper.vm.navigateForward = jest.fn();
wrapper.vm.previouslyEnteredCarId = 'new carId';
// Act
await wrapper.vm.forwardButtonAction();
//Assert
expect(wrapper.vm.navigateForward).not.toHaveBeenCalled();
});
it("Should not call navigateForward() when forward button is clicked but lookupVehicle errors out.", async () => {
// Arrange
const { wrapper } = setupMocks({});
const vehicleLookupApiResponse = {
data: {
carId: 'new carId' // does not match the store value
}
};
const vinPromise = Promise.reject(vehicleLookupApiResponse);
wrapper.vm.lookupVehicle = jest.fn().mockImplementation(() => vinPromise);
wrapper.vm.navigateForward = jest.fn();
wrapper.vm.previouslyEnteredCarId = 'new carId';
// Act
await wrapper.vm.forwardButtonAction();
//Assert
expect(wrapper.vm.navigateForward).not.toHaveBeenCalled();
});
describe("navigateForward", () => {
test("carId is different from returned vehicle and selected glass isn't available => continue with different glass", async () => {
// Arrange
const { wrapper } = setupMocks({
customMountOptions: {
router: {
navigateAfterSave: jest.fn()
}
}
});
wrapper.vm.navigateForwardWithSingleCarMatch = jest.fn();
wrapper.setData({
isCarIdDifferent: true,
isSelectedGlassAvailableForVehicle: false
});
// Act
await wrapper.vm.navigateForward();
//Assert
expect(wrapper.vm.$router.navigateAfterSave).toBeCalledTimes(1);
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledWith(navigationScenarios.SELECTED_VIN_HAS_MISMATCHED_GLASS, wrapper.vm.$route, expect.anything(), expect.anything(), expect.anything());
})
test("carId matches => navigateForwardWithSingleCarMatch", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.navigateForwardWithSingleCarMatch = jest.fn();
wrapper.setData({
isCarIdDifferent: false,
});
// Act
await wrapper.vm.navigateForward();
//Assert
expect(wrapper.vm.navigateForwardWithSingleCarMatch).toBeCalledTimes(1);
})
test("selected glass is available for returned vehicle => navigateForwardWithSingleCarMatch", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.navigateForwardWithSingleCarMatch = jest.fn();
wrapper.setData({
isSelectedGlassAvailableForVehicle: true,
});
// Act
await wrapper.vm.navigateForward();
//Assert
expect(wrapper.vm.navigateForwardWithSingleCarMatch).toBeCalledTimes(1);
})
})
});
function setupMocks({ customMountOptions }) {
const mountOptions = getMountOptions({
...customMountOptions
});
// Modify/augment default mount options
mountOptions.global.mocks["$store"] = store;
mountOptions.global.mixins = [mockMixin];
mountOptions['attachTo'] = document.body; // append wrapper to document.body to test DOM methods
const wrapper = shallowMount(vinLookup, mountOptions);
mockOutPromises(wrapper);
mockOutStubFunctions(wrapper);
return { wrapper };
}
function mockOutPromises(wrapper) {
const zipValidationApiResponse = {
data: {
isServiceable: true
}
};
const vehicleLookupApiResponse = {
data: {
carId: 'initial carId'
}
};
const zipPromise = Promise.resolve(zipValidationApiResponse);
const vinPromise = Promise.resolve(vehicleLookupApiResponse);
wrapper.vm.validateZip = jest.fn().mockImplementation(() => zipPromise);
wrapper.vm.lookupVehicle = jest.fn().mockImplementation(() => vinPromise);
}
function mockOutStubFunctions(wrapper) {
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn();
}
const mockMixin = {
methods: {
getCmsContent: jest.fn(() => "placeholder CMS content"),
}
}

View file

@ -8,7 +8,7 @@
<div class="page-container-grouped-styles">
<loadingModal ref="loadingModal"/>
<funnelHeader cmsWidgetName="FunnelHeaderWidget" />
<vehicleBanner
<vehicleBanner
cmsWidgetName="VehicleBannerWidget"
:displayGenericVehicleImage="false"
/>
@ -134,7 +134,6 @@ import loadingModal from '@/common-components/loading-modal/loading-modal.vue';
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import store from "@/store";
import baseMixin from "@/mixins/base-mixin.js";
import { storeActions } from "@/constants/store-actions";
import { storeMutations } from "@/constants/store-mutations";
import { errorMessages } from "@/constants/error-messages";
@ -143,6 +142,7 @@ import { required, regex } from "@/helpers/validation-rules";
import { Form, defineRule } from "vee-validate";
import { navigateAfterSaveToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { getFunnelCookie } from "@/helpers/heritage-integration/cookie-helper";
import vinPagesMixin from "@/mixins/vin-pages-mixin";
// DEFINE VALIDATION RULES
defineRule("zip-required", required(errorMessages.SERVICE_ZIP_REQUIRED));
@ -154,6 +154,7 @@ defineRule("vin-format", regex(/^[a-hA-Hj-nJ-NpPr-zR-Z0-9]{17}$/, errorMessages.
export default {
name: "vin-lookup",
mixins: [vinPagesMixin],
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.fmgPage);
@ -313,9 +314,13 @@ export default {
this.vinNotFound = true;
this.$refs.funnelFooter.removeLoader();
this.noServiceZip = false;
return;
return false;
});
if (!vehicleLookupResponse) {
return;
}
if (!zipValidationResponse.data.isServiceable) {
this.customAlertData.zip = this.zip;
this.$refs.funnelFooter.removeLoader();
@ -343,22 +348,21 @@ export default {
},
navigateForward(){
if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle){
this.$router.navigateAfterSave(this.navigationScenarios.CLICKED_FORWARD, this.$route, {}, { displayVehicleChangeAlert: true }, {});
if (this.isCarIdDifferent && !this.isSelectedGlassAvailableForVehicle) {
this.$router.navigateAfterSave(this.navigationScenarios.SELECTED_VIN_HAS_MISMATCHED_GLASS, this.$route, {}, { displayVehicleChangeAlert: true }, {});
return;
} else {
this.$refs.loadingModal.showModal();
navigateAfterSaveToHeritageFunnel(this.$route);
this.navigateForwardWithSingleCarMatch();
return;
}
},
validateZip(zip) {
return baseMixin.methods.dispatchStoreAction(storeActions.VALIDATE_ZIP, {
return this.dispatchStoreAction(storeActions.VALIDATE_ZIP, {
zip,
});
},
lookupVehicle(vin) {
return baseMixin.methods.dispatchStoreAction(
return this.dispatchStoreAction(
storeActions.LOOKUP_VEHICLE_BY_VIN,
{ vin }
);

View file

@ -124,6 +124,32 @@ describe("analyticsMixin.js", () => {
//Assert
expect(obj!=null);
});
test("Obj method name does not include bound", () => {
//Arrange
const obj = {baseMethodName:"testMethodName", data:"testData"};
const method = {name:"testMethodName", data:"testData" }
const action = "testAction";
//Act
analyticsMixin.methods.prependActionToMethod(obj, method, action);
//Assert
expect(method.name.startsWith("bound ")).toBe(false);
});
test("Prepended action does not include bound", () => {
//Arrange
const obj = {baseMethodName:"testMethodName", data:"testData"};
const method = {name:"testMethodName", data:"testData" }
const action = "testAction";
//Act
analyticsMixin.methods.prependActionToMethod(obj, method, action);
//Assert
expect(action.startsWith("bound ")).toBe(false);
});
test("analyticsPageEvents returns constants analyticsPageEvents", () => {
//Act

View file

@ -0,0 +1,28 @@
import store from "@/store";
import { storeActions } from "@/constants/store-actions.js";
import { storeMutations } from "@/constants/store-mutations.js";
import { navigateAfterSaveToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
export default {
methods: {
async navigateForwardWithSingleCarMatch() {
const result = await this.dispatchStoreAction(storeActions.GET_PARTS_OR_QUESTIONS);
const partsOrQuestions = result.data.partsOrQuestions;
const hasPartsQuestions = partsOrQuestions.some(pq => pq.partQuestions?.length > 0);
const hasGlassLocationWithMultipleParts = partsOrQuestions.some(pq => pq.parts?.length > 1);
if (hasPartsQuestions) {
this.$router.navigateAfterSave(this.navigationScenarios.SELECTED_VIN_WITH_PART_QUESTIONS, this.$route, {}, {}, result.data);
}
else if (hasGlassLocationWithMultipleParts) {
this.$router.navigateAfterSave(this.navigationScenarios.SELECTED_VIN_WITH_MULTIPLE_PARTS, this.$route, {}, {}, result.data);
}
else {
store.commit(storeMutations.UPDATE_PARTS, result.data);
this.$refs.loadingModal.showModal();
navigateAfterSaveToHeritageFunnel(this.$route);
}
}
}
}

View file

@ -0,0 +1,921 @@
import vinPagesMixin from "@/mixins/vin-pages-mixin";
import { shallowMount } from "@vue/test-utils";
import { setupMocksForJsFiles, getMountOptions } from "@/helpers/unit-test-helper.js";
import { storeActions } from "@/constants/store-actions";
import { storeMutations } from "@/constants/store-mutations";
import { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
import store from "@/store";
import loadingModal from '@/common-components/loading-modal/loading-modal.vue';
import { navigateAfterSaveToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({
navigateAfterSaveToHeritageFunnel: jest.fn()
}));
describe("vin-pages-mixin", () => {
afterEach(() => {
jest.clearAllMocks();
})
describe("navigateForwardWithSingleCarMatch", () => {
describe("should go to parts-questions", () => {
test("single glass location has part question => go to parts-questions", async () => {
// Arrange
const partsOrQuestions = [
{
"glassName": "Single",
"glassLocation": "Windshield",
"parts": null,
"partQuestions": [
{
"questionSequence": 1,
"questionText": "Is there a line running across the bottom, driver's side then up the center of the Windshield?",
"answers": [
{
"answerText": "Yes",
"nextQuestionSequence": null,
"answerResult": "DW01144"
},
{
"answerText": "No",
"nextQuestionSequence": null,
"answerResult": "DW01143"
}
]
}
]
}
];
const { wrapper } = setupMocks({
partsOrQuestions: partsOrQuestions
});
// Act
await wrapper.vm.navigateForwardWithSingleCarMatch();
// Assert
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledWith(navigationScenarios.SELECTED_VIN_WITH_PART_QUESTIONS, wrapper.vm.$route, {}, {}, { partsOrQuestions });
});
test("multiple glass locations have part questions => go to parts-questions", async () => {
// Arrange
const partsOrQuestions = [
{
"glassName": "Single",
"glassLocation": "Windshield",
"parts": null,
"partQuestions": [
{
"questionSequence": 1,
"questionText": "Is there a line running across the bottom, driver's side then up the center of the Windshield?",
"answers": [
{
"answerText": "Yes",
"nextQuestionSequence": null,
"answerResult": "DW01144"
},
{
"answerText": "No",
"nextQuestionSequence": null,
"answerResult": "DW01143"
}
]
}
]
},
{
"glassName": "Front",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "DD08158GTYN",
"description": "driver side, front",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "Quarter",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "DQ08162GTYN",
"description": "driver side, 1 hole",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "SideDoor",
"glassLocation": "Driver",
"parts": null,
"partQuestions": [
{
"questionSequence": 2,
"questionText": "Is this a super awesome question?",
"answers": [
{
"answerText": "Yes",
"nextQuestionSequence": null,
"answerResult": "DW01144000"
},
{
"answerText": "Super yes",
"nextQuestionSequence": null,
"answerResult": "DW01143001"
}
]
}
]
},
{
"glassName": "Stationary",
"glassLocation": "Rear",
"parts": [
{
"partNumber": "DB08165GTNN",
"description": "heated glass, stationary",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
}
];
const { wrapper } = setupMocks({
partsOrQuestions: partsOrQuestions
});
// Act
await wrapper.vm.navigateForwardWithSingleCarMatch();
// Assert
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledWith(navigationScenarios.SELECTED_VIN_WITH_PART_QUESTIONS, wrapper.vm.$route, {}, {}, { partsOrQuestions });
});
test("multiple glass locations selected, one has part question => go to parts-questions", async () => {
// Arrange
const partsOrQuestions = [
{
"glassName": "Single",
"glassLocation": "Windshield",
"parts": null,
"partQuestions": [
{
"questionSequence": 1,
"questionText": "Is there a line running across the bottom, driver's side then up the center of the Windshield?",
"answers": [
{
"answerText": "Yes",
"nextQuestionSequence": null,
"answerResult": "DW01144"
},
{
"answerText": "No",
"nextQuestionSequence": null,
"answerResult": "DW01143"
}
]
}
]
},
{
"glassName": "Front",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "DD08158GTYN",
"description": "driver side, front",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "Quarter",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "DQ08162GTYN",
"description": "driver side, 1 hole",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "SideDoor",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "DD08160GTYN",
"description": "driver side, body side, 1 hole",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "Stationary",
"glassLocation": "Rear",
"parts": [
{
"partNumber": "DB08165GTNN",
"description": "heated glass, stationary",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
}
];
const { wrapper } = setupMocks({
partsOrQuestions: partsOrQuestions
});
// Act
await wrapper.vm.navigateForwardWithSingleCarMatch();
// Assert
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledWith(navigationScenarios.SELECTED_VIN_WITH_PART_QUESTIONS, wrapper.vm.$route, {}, {}, { partsOrQuestions });
});
test("a selected glass location has part questions and multiple parts => go to parts-questions", async () => {
// Arrange
const partsOrQuestions = [
{
"glassName": "Single",
"glassLocation": "Windshield",
"parts": null,
"partQuestions": [
{
"questionSequence": 1,
"questionText": "Is there a line running across the bottom, driver's side then up the center of the Windshield?",
"answers": [
{
"answerText": "Yes",
"nextQuestionSequence": null,
"answerResult": "DW01144"
},
{
"answerText": "No",
"nextQuestionSequence": null,
"answerResult": "DW01143"
}
]
}
]
},
{
"glassName": "Front",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "DD08158GTYN",
"description": "driver side, front",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "Quarter",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "DQ08162GTYN",
"description": "driver side, 1 hole",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DQ08162YPYN",
"description": "driver side, 1 hole",
"color": "Gray Tint Privacy",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "SideDoor",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "DD08160GTYN",
"description": "driver side, body side, 1 hole",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DD08160YPYN",
"description": "driver side, body side, 1 hole",
"color": "Gray Tint Privacy",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "Stationary",
"glassLocation": "Rear",
"parts": [
{
"partNumber": "DB08165GTNN",
"description": "heated glass, stationary",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DB08165YPNN",
"description": "heated glass, stationary",
"color": "Gray Tint Privacy",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DB08166GTNN",
"description": "stationary",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DB08167GTNN",
"description": "heated glass, movable, 8 hole",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DB08167YPNN",
"description": "heated glass, movable, 8 hole",
"color": "Gray Tint Privacy",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
}
];
const { wrapper } = setupMocks({
partsOrQuestions: partsOrQuestions
});
// Act
await wrapper.vm.navigateForwardWithSingleCarMatch();
// Assert
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledWith(navigationScenarios.SELECTED_VIN_WITH_PART_QUESTIONS, wrapper.vm.$route, {}, {}, { partsOrQuestions });
});
});
describe("should go to vehicle-parts", () => {
test("single glass location has multiple parts => go to vehicle-parts", async () => {
// Arrange
const partsOrQuestions = [
{
"glassName": "Stationary",
"glassLocation": "Rear",
"parts": [
{
"partNumber": "FB25724GTYN",
"description": "heated glass, solar, antenna",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "FB25759GTYN",
"description": "heated glass, solar, antenna, w/diversity antenna",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
}
];
const { wrapper } = setupMocks({
partsOrQuestions: partsOrQuestions
});
// Act
await wrapper.vm.navigateForwardWithSingleCarMatch();
// Assert
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledWith(navigationScenarios.SELECTED_VIN_WITH_MULTIPLE_PARTS, wrapper.vm.$route, {}, {}, { partsOrQuestions });
});
test("multiple glass locations selected, one of them has multiple parts => go to vehicle parts", async () => {
// Arrange
const partsOrQuestions = [
{
"glassName": "Single",
"glassLocation": "Windshield",
"parts": [
{
"partNumber": "FW03647GTNN",
"description": "solar, 3rd visor band",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": [
{
"partNumber": "MWF03647",
"partType": "MOULDING",
"description": "Upper "
}
]
}
],
"partQuestions": null
},
{
"glassName": "Back",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "FD25747GTYN",
"description": "solar, driver side, rear, ex models and above",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "Front",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "FD25719GTYN",
"description": "solar, driver side, front, ex models and above",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "Vent",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "FV25749GTNN",
"description": "solar, driver side, rear",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "Stationary",
"glassLocation": "Rear",
"parts": [
{
"partNumber": "FB25724GTYN",
"description": "heated glass, solar, antenna",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "FB25759GTYN",
"description": "heated glass, solar, antenna, w/diversity antenna",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
}
];
const { wrapper } = setupMocks({
partsOrQuestions: partsOrQuestions
});
// Act
await wrapper.vm.navigateForwardWithSingleCarMatch();
// Assert
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledWith(navigationScenarios.SELECTED_VIN_WITH_MULTIPLE_PARTS, wrapper.vm.$route, {}, {}, { partsOrQuestions });
});
test("multiple glass locations selected, multiple have multiple parts => go to vehicle-parts", async () => {
// Arrange
const partsOrQuestions = [
{
"glassName": "Single",
"glassLocation": "Windshield",
"parts": [
{
"partNumber": "DW02101GTYN",
"description": "solar",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "Back",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "DD12202GTYN",
"description": "solar, driver side, rear",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DD12202YPYN",
"description": "solar, driver side, rear",
"color": "Gray Tint Privacy",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "Front",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "DD12198GTYN",
"description": "solar, driver side, front",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DD12200GTYN",
"description": "solar, driver side, front, laminated, soundproofing",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "Quarter",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "DQ12204GTYNOEM",
"description": "solar, driver side, encap",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DQ12204YPYNOEM",
"description": "solar, driver side, encap",
"color": "Gray Tint Privacy",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DQ12205GTYNOEM",
"description": "solar, antenna, driver side, encap",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DQ12205YPYNOEM",
"description": "solar, antenna, driver side, encap",
"color": "Gray Tint Privacy",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DQ12207GTYN",
"description": "solar, driver side, encap, chrome molding",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DQ12207YPYNOEM",
"description": "solar, driver side, encap, chrome molding",
"color": "Gray Tint Privacy",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DQ12208GTYNOEM",
"description": "solar, antenna, driver side, encap, chrome molding",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DQ12208YPYNOEM",
"description": "solar, antenna, driver side, encap, chrome molding",
"color": "Gray Tint Privacy",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "Stationary",
"glassLocation": "Rear",
"parts": [
{
"partNumber": "DB12209GTYN",
"description": "heated glass, solar, 1 hole",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
},
{
"partNumber": "DB12209YPYN",
"description": "heated glass, solar, 1 hole",
"color": "Gray Tint Privacy",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
}
];
const { wrapper } = setupMocks({
partsOrQuestions: partsOrQuestions
});
// Act
await wrapper.vm.navigateForwardWithSingleCarMatch();
// Assert
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledWith(navigationScenarios.SELECTED_VIN_WITH_MULTIPLE_PARTS, wrapper.vm.$route, {}, {}, { partsOrQuestions });
});
});
describe("should go to heritage funnel", () => {
test("single glass location selected, has no part questions and has one part => go to heritage funnel", async () => {
// Arrange
const partsOrQuestions = [
{
"glassName": "Single",
"glassLocation": "Windshield",
"parts": [
{
"partNumber": "FW04186GTYN",
"description": "solar, soundproofing, lane keep assist",
"color": "Green Tint",
"requiresRecalibration": true,
"requiresCapabilityQuestions": false,
"childParts": [
{
"partNumber": "GGG 3563 KIT",
"partType": "MOULDING",
"description": "Kit, Top & Sides "
}
]
}
],
"partQuestions": null
}
]
const { wrapper } = setupMocks({
partsOrQuestions: partsOrQuestions
});
// Act
await wrapper.vm.navigateForwardWithSingleCarMatch();
// Assert
expect(navigateAfterSaveToHeritageFunnel).toHaveBeenCalledTimes(1);
});
test("multiple glass locations selected, each has one part and no part questions => go to heritage funnel", async () => {
// Arrange
const partsOrQuestions = [
{
"glassName": "Single",
"glassLocation": "Windshield",
"parts": [
{
"partNumber": "FW04186GTYN",
"description": "solar, soundproofing, lane keep assist",
"color": "Green Tint",
"requiresRecalibration": true,
"requiresCapabilityQuestions": false,
"childParts": [
{
"partNumber": "GGG 3563 KIT",
"partType": "MOULDING",
"description": "Kit, Top & Sides "
}
]
}
],
"partQuestions": null
},
{
"glassName": "Back",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "FD25457GTYN",
"description": "solar, driver side, rear",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "Front",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "FD27090GTYN",
"description": "solar, driver side, front",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "Vent",
"glassLocation": "Driver",
"parts": [
{
"partNumber": "FV25459GTNN",
"description": "solar, driver side, rear",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
},
{
"glassName": "Stationary",
"glassLocation": "Rear",
"parts": [
{
"partNumber": "FB25460GTYN",
"description": "heated glass, solar",
"color": "Green Tint",
"requiresRecalibration": false,
"requiresCapabilityQuestions": false,
"childParts": null
}
],
"partQuestions": null
}
];
const { wrapper } = setupMocks({
partsOrQuestions: partsOrQuestions
});
// wrapper.vm.navigateAfterSaveToHeritageFunnel = jest.fn();
store.commit = jest.fn();
// Act
await wrapper.vm.navigateForwardWithSingleCarMatch();
// Assert
expect(store.commit).toHaveBeenCalledTimes(1);
expect(store.commit).toHaveBeenCalledWith(storeMutations.UPDATE_PARTS, { partsOrQuestions })
expect(wrapper.vm.$refs.loadingModal.showModal).toHaveBeenCalledTimes(1);
expect(navigateAfterSaveToHeritageFunnel).toHaveBeenCalledTimes(1);
});
});
});
});
function setupMocks({ partsOrQuestions = [] }) {
const baseMixin = setupMocksForJsFiles({
actionList: [
{
actionName: storeActions.GET_PARTS_OR_QUESTIONS,
data: {
partsOrQuestions: partsOrQuestions
}
},
],
});
const mocks = getMountOptions({
router: {
navigateAfterSave: jest.fn()
},
});
const mockVinComponent = {
components: { loadingModal },
template: '<loadingModal ref="loadingModal" />',
// render() {
// return '<div ref="loadingModal"></div>'
// },
mixins: [vinPagesMixin, baseMixin.baseMixin]
};
// const mockVinComponent = Vue.component("mockcomponent", {
// template: '<loadingModal ref="loadingModal" />',
// mixins: [vinPagesMixin, baseMixin.baseMixin]
// })
const wrapper = shallowMount(mockVinComponent, mocks);
wrapper.vm.$refs.loadingModal.showModal = jest.fn();
return { wrapper };
}

View file

@ -8,11 +8,11 @@ const navigationScenarios = {
CLICKED_FORWARD: "CLICKED_FORWARD",
CLICKED_FORWARD_WITH_VIN: "CLICKED_FORWARD_WITH_VIN",
SELECTED_PARTS: "SELECTED_PARTS",
CONTINUING_WITH_PARTS_QUESTION: "CONTINUING_WITH_PARTS_QUESTION",
CONTINUING_WITH_MULTIPLE_PARTS: "CONTINUING_WITH_MULTIPLE_PARTS",
SELECTED_VIN_WITH_PART_QUESTIONS: "SELECTED_VIN_WITH_PART_QUESTIONS",
SELECTED_VIN_WITH_MULTIPLE_PARTS: "SELECTED_VIN_WITH_MULTIPLE_PARTS",
CONTINUING_WITH_SINGLE_PART: "CONTINUING_WITH_SINGLE_PART",
CONTINUING_WITH_MULTIPLE_VEHICLES: "CONTINUING_WITH_MULTIPLE_VEHICLES",
CONTINUING_WITH_DIFFERENT_GLASS: "CONTINUING_WITH_DIFFERENT_GLASS",
SELECTED_VIN_HAS_MISMATCHED_GLASS: "SELECTED_VIN_HAS_MISMATCHED_GLASS",
CLICKED_FORWARD_WITHOUT_VIN: "CLICKED_FORWARD_WITHOUT_VIN",
SELECTED_MANUAL_VIN: "SELECTED_MANUAL_VIN",
SELECTED_LICENSE_PLATE: "SELECTED_LICENSE_PLATE",

View file

@ -72,7 +72,7 @@ const routingTable = [
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
destinationFmgPageValue: fmgPageValues.VIN_LOOKUP,
},
{
scenario: navigationScenarios.SELECTED_PARTS,
@ -105,12 +105,20 @@ const routingTable = [
destinationFmgPageValue: fmgPageValues.ESTIMATE,
},
{
scenario: navigationScenarios.CLICKED_FORWARD,
scenario: navigationScenarios.SELECTED_VIN_HAS_MISMATCHED_GLASS,
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
},
{
scenario: navigationScenarios.CLICKED_BACK_WITH_VIN,
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
},
{
scenario: navigationScenarios.SELECTED_VIN_WITH_PART_QUESTIONS,
destinationFmgPageValue: fmgPageValues.PART_QUESTIONS
},
{
scenario: navigationScenarios.SELECTED_VIN_WITH_MULTIPLE_PARTS,
destinationFmgPageValue: fmgPageValues.VEHICLE_PARTS
}
],
},
@ -125,6 +133,14 @@ const routingTable = [
scenario: navigationScenarios.CLICKED_FORWARD,
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
},
{
scenario: navigationScenarios.SELECTED_VIN_WITH_PART_QUESTIONS,
destinationFmgPageValue: fmgPageValues.PART_QUESTIONS
},
{
scenario: navigationScenarios.SELECTED_VIN_WITH_MULTIPLE_PARTS,
destinationFmgPageValue: fmgPageValues.VEHICLE_PARTS
}
],
},
{
@ -139,9 +155,17 @@ const routingTable = [
destinationFmgPageValue: fmgPageValues.ADDRESS_VEHICLES,
},
{
scenario: navigationScenarios.CONTINUING_WITH_DIFFERENT_GLASS,
scenario: navigationScenarios.SELECTED_VIN_HAS_MISMATCHED_GLASS,
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
},
{
scenario: navigationScenarios.SELECTED_VIN_WITH_PART_QUESTIONS,
destinationFmgPageValue: fmgPageValues.PART_QUESTIONS
},
{
scenario: navigationScenarios.SELECTED_VIN_WITH_MULTIPLE_PARTS,
destinationFmgPageValue: fmgPageValues.VEHICLE_PARTS
}
],
},
{
@ -154,7 +178,15 @@ const routingTable = [
{
scenario: navigationScenarios.CLICKED_FORWARD,
destinationFmgPageValue: fmgPageValues.VEHICLE_DAMAGE,
},
},
{
scenario: navigationScenarios.SELECTED_VIN_WITH_PART_QUESTIONS,
destinationFmgPageValue: fmgPageValues.PART_QUESTIONS
},
{
scenario: navigationScenarios.SELECTED_VIN_WITH_MULTIPLE_PARTS,
destinationFmgPageValue: fmgPageValues.VEHICLE_PARTS
}
],
},
{
@ -178,6 +210,15 @@ const routingTable = [
},
],
},
{
fmgPageValue: fmgPageValues.PART_QUESTIONS,
maps: [
{
scenario: navigationScenarios.CLICKED_BACK,
destinationFmgPageValue: fmgPageValues.VIN_LOOKUP
}
]
},
];
export { routingTable };

View file

@ -539,7 +539,16 @@ export const actions = {
// Parts API Actions
getPartsOrQuestions(context, { carId, glassArray, zipCode, vin = '' }) {
getPartsOrQuestions(context) {
const vehicle = context.getters.vehicle;
const damage = context.getters.damage;
const order = context.state.order;
const carId = vehicle.carId;
const glassArray = damage.glassToReplace;
const zipCode = order.serviceLocation.zipCode;
const vin = vehicle.vin;
return globalMethods.callHttpClient({
method: endpoints.GetPartsOrQuestions.method,
endpoint: endpoints.GetPartsOrQuestions.url,