check in so I can pull develop into this branch.

This commit is contained in:
Jason Wheeler 2023-01-10 09:07:35 -05:00
parent 4997029773
commit d53b7e276c
12 changed files with 2404 additions and 80 deletions

View file

@ -8,6 +8,7 @@ const applicationConfig = {
SITE_ENTRY_TRIGGER_VALUE: "SelfService",
APPLICATION_ABBREVIATION: "iss",
PAGE_QUERYSTRING: 'issPage',
GOOGLE_PLACES_API_KEY: process.env.VUE_APP_GOOGLE_PLACES_API_KEY,
};
export { applicationConfig };

View file

@ -18,8 +18,8 @@ const errorMessages = {
LAST_NAME_REQUIRED: "Please enter your last name",
EMAIL_ADDRESS_REQUIRED: "Please enter your email address",
EMAIL_ADDRESS_FORMAT: "Please enter a valid email address",
SERVICE_ZIP_REQUIRED: "Please enter your service ZIP",
SERVICE_ZIP_FORMAT: "Please enter a valid service ZIP",
SERVICE_ZIP_REQUIRED: "Please enter your ZIP",
SERVICE_ZIP_FORMAT: "Please enter a valid ZIP",
VIN_REQUIRED: "Please enter your VIN",
VIN_FORMAT: "Invalid VIN. Please make sure that you entered the correct 17-digit, alpha-numeric number. VINs do not contain the letters I, O, or Q",
OPTION_REQUIRED: "Please select an option",

View file

@ -0,0 +1,730 @@
// Components
import addressLookup from "@/layouts/address-lookup/address-lookup.vue";
// Supporting Files
import { settleAllPromises } from "@/helpers/layout-helper.js";
import { 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 { navigationScenarios } from "@/router/router-constants/navigation-scenarios";
import store from "@/store";
import { navigateToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
jest.mock("@/helpers/damage-helper", () => ({
isGlassAvailableForCarId: jest.fn().mockImplementation(() => true),
getDamageString: jest.fn(),
}));
jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({
navigateToHeritageFunnel: jest.fn(),
}));
// Mock our module for promises.
jest.mock("@/helpers/layout-helper.js", () => ({
settleAllPromises: jest.fn(),
}));
describe("address-lookup.vue", () => {
describe("page level alerts", () => {
test("if the address is not serviceable display the Non-Serviceable Zip Alert", async () => {
// Arrange
const mockRegistrationAddress = {
streetAddress: "1234 Main St",
city: "Columbus",
state: "OH",
zipCode: "43215",
};
const { wrapper } = setupMocks({
isZipValid: true,
isZipServiceable: false,
vinVehicles: [
{
vin: "TEST_VIN",
vehicle: {
carId: "C0000",
},
},
],
});
store.commit(storeMutations.UPDATE_CAR_ID, "CARID");
await wrapper.setData({
customerQuestions: {
addressQuestions: mockRegistrationAddress,
},
});
// Act
await wrapper.vm.forwardButtonAction();
// Assert
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 = {
streetAddress: "1234 Main St",
city: "Columbus",
state: "OH",
zipCode: "43215",
};
const { wrapper } = setupMocks({
isZipServiceable: true,
vinVehicles: [
{
vehicle: {
carId: "C00000",
},
},
],
});
store.commit(storeMutations.UPDATE_CAR_ID, "CARID2");
await wrapper.setData({
customerQuestions: {
addressQuestions: mockRegistrationAddress,
},
});
// Act
await wrapper.vm.forwardButtonAction();
// Assert
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
const mockRegistrationAddress = {
streetAddress: "1234 Main St",
city: "Columbus",
state: "OH",
zipCode: "43215",
};
const { wrapper } = setupMocks({
isZipServiceable: true,
isStatePermissible: false,
lookupVinbyAddressResponse: {
isStatePermissible: false,
vinVehicles: [
{
vin: "TEST_VIN",
vehicle: {
carId: "CARID",
},
},
{
vin: "TEST_VIN2",
vehicle: {
carId: "CARID2",
},
},
],
},
});
store.commit(storeMutations.UPDATE_CAR_ID, "CARID");
await wrapper.setData({
customerQuestions: {
addressQuestions: mockRegistrationAddress,
},
});
// Act
await wrapper.vm.forwardButtonAction();
// Assert
expect(
wrapper.findComponent({ ref: "alertVinLookupsByHomeAddressNotAllowed" }).isVisible()
).toBe(true);
});
test("if no vehicles found, display Vin Not Found alert", async () => {
// Arrange
const mockRegistrationAddress = {
streetAddress: "1234 Main St",
city: "Columbus",
state: "OH",
zipCode: "43215",
};
const { wrapper } = setupMocks({
isZipServiceable: true,
lookupVinbyAddressResponse: {
isStatePermissible: true,
vinVehicles: [], // Return no vehicles
},
});
store.commit(storeMutations.UPDATE_CAR_ID, "CARID");
await wrapper.setData({
customerQuestions: {
addressQuestions: mockRegistrationAddress,
},
});
wrapper.vm.navigateForward = jest.fn();
// Act
await wrapper.vm.forwardButtonAction();
// Assert
expect(wrapper.findComponent({ ref: "alertVinNotFound" }).isVisible()).toBe(true);
});
});
describe("navigation", () => {
test("if the back button is clicked, navigate back", async () => {
// Arrange
const { wrapper } = setupMocks({
isZipServiceable: true,
});
// Act
await wrapper.vm.backButtonAction();
// Assert
expect(wrapper.vm.$router.navigateWithoutSaving).toHaveBeenCalled();
});
test("if the car entered matches one of the vehicles found and the zip is serviceable, navigate forward", async () => {
// Arrange
const mockRegistrationAddress = {
streetAddress: "1234 Main St",
city: "Columbus",
state: "OH",
zipCode: "43215",
};
const { wrapper } = setupMocks({
isZipServiceable: true,
vinVehicles: [
{
vehicle: {
carId: "C11111",
},
},
],
});
await wrapper.setData({
previouslyEnteredCarId: "C11111",
customerQuestions: {
addressQuestions: mockRegistrationAddress,
},
});
wrapper.vm.navigateForward = jest.fn();
// Act
await wrapper.vm.forwardButtonAction();
// Assert
expect(wrapper.vm.navigateForward).toHaveBeenCalled();
});
test("if the car entered does not match any of the multiple vehicles found, navigate to address-vehicles page", async () => {
// Arrange
const mockRegistrationAddress = {
streetAddress: "1234 Main St",
city: "Columbus",
state: "OH",
zipCode: "43215",
};
const { wrapper } = setupMocks({
isZipServiceable: true,
isStatePermissible: true,
vinVehicles: [
{
vin: "TEST_VIN",
vehicle: {
carId: "CARID",
},
},
{
vin: "TEST_VIN2",
vehicle: {
carId: "CARID2",
},
},
],
});
store.commit(storeMutations.UPDATE_CAR_ID, "CARID_A");
const carsFound = [
{
vin: "TEST_VIN",
vehicle: {
carId: "CARID",
},
},
{
vin: "TEST_VIN2",
vehicle: {
carId: "CARID2",
},
},
];
await wrapper.setData({
customerQuestions: {
addressQuestions: mockRegistrationAddress,
},
});
wrapper.vm.updateVehicleInfo = jest.fn();
// Act
await wrapper.vm.forwardButtonAction();
// Assert
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalledWith(
navigationScenarios.CLICKED_FORWARD_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
const mockRegistrationAddress = {
streetAddress: "1234 Main St",
city: "Columbus",
state: "OH",
zipCode: "43215",
};
const { wrapper } = setupMocks({
isZipServiceable: false,
isStatePermissible: true,
vinVehicles: [
{
vin: "TEST_VIN",
vehicle: {
carId: "CARID",
},
},
{
vin: "TEST_VIN2",
vehicle: {
carId: "CARID2",
},
},
],
});
store.commit(storeMutations.UPDATE_CAR_ID, "CARID");
await wrapper.setData({
customerQuestions: {
addressQuestions: mockRegistrationAddress,
},
});
wrapper.vm.navigateForward = jest.fn();
// Act
await wrapper.vm.forwardButtonAction();
// 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
const mockRegistrationAddress = {
streetAddress: "1234 Main St",
city: "Columbus",
state: "OH",
zipCode: "43215",
};
const { wrapper } = setupMocks({
isZipServiceable: true,
isStatePermissible: true,
});
await wrapper.setData({
customerQuestions: {
addressQuestions: mockRegistrationAddress,
},
isCarIdDifferent: true,
isSelectedGlassAvailableForVehicle: false,
});
let carsFound = [
{
vin: "TEST_VIN2",
vehicle: {
carId: "C0000",
},
},
];
// Act
await wrapper.vm.navigateForward(carsFound);
// Assert
expect(wrapper.vm.$router.navigateWithSaving).toHaveBeenCalledWith(
navigationScenarios.SELECTED_VIN_WITH_MISMATCHED_GLASS,
undefined,
{},
{ displayVehicleChangeAlert: true }
);
});
test("single car was found and matches entered vehicle => navigateForwardWithSingleCarMatch", async () => {
// Arrange
const carsFound = [
{
vin: "TEST_VIN_2",
vehicle: {
carId: "C0000",
},
},
];
const { wrapper } = setupMocks({}, {});
wrapper.vm.navigateForwardWithSingleCarMatch = jest.fn();
// Act
wrapper.vm.navigateForward(carsFound);
// Assert
expect(wrapper.vm.navigateForwardWithSingleCarMatch).toHaveBeenCalledTimes(1);
});
test("multiple cars were found and one matches entered vehicle => navigateForwardWithSingleCarMatch", async () => {
// Arrange
const carsFound = [
{
vin: "TEST_VIN_1",
vehicle: {
carId: "C0000",
},
},
{
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(carsFound);
// Assert
expect(wrapper.vm.navigateForwardWithSingleCarMatch).toHaveBeenCalledTimes(1);
});
});
describe("registration and service zips", () => {
describe("if registration zip is serviceable", () => {
test("if registration address is provided => update service address on successful continue", async () => {
// Arrange
const mockRegistrationAddress = {
streetAddress: "1234 Main St",
city: "Columbus",
state: "OH",
zipCode: "43215",
};
const { wrapper } = setupMocks({
isZipServiceable: true,
});
store.commit(storeMutations.UPDATE_CAR_ID, "CARID");
await wrapper.setData({
customerQuestions: {
addressQuestions: mockRegistrationAddress,
},
});
// Act
await wrapper.vm.forwardButtonAction();
// Assert
expect(wrapper.vm.dispatchStoreAction).toHaveBeenCalledWith(
"lookupVinByAddress",
{
licenseLastName: undefined,
licenseState: "OH",
licenseStreetAddress: "1234 Main St",
licenseZip: "43215",
},
false
);
expect(wrapper.vm.dispatchStoreAction).toHaveBeenCalledWith("validateZip", {
zip: "43215",
});
});
});
describe("if registration zip is not serviceable", () => {
test("if registration address is provided and user clicks continue => show non-serviceable zip alert", async () => {
// Arrange
const mockRegistrationAddress = {
streetAddress: "1234 Main St",
city: "Columbus",
state: "OH",
zipCode: "43215",
};
const { wrapper } = setupMocks({
isZipValid: true,
isZipServiceable: false,
vinVehicles: [
{
vin: "TEST_VIN",
vehicle: {
carId: "C0000",
},
},
],
});
store.commit(storeMutations.UPDATE_CAR_ID, "CARID");
await wrapper.setData({
customerQuestions: {
addressQuestions: mockRegistrationAddress,
},
});
expect(wrapper.findComponent({ ref: "alertNonServiceableZip" }).exists()).toBe(
false
);
// Act
await wrapper.vm.forwardButtonAction();
// Assert
expect(wrapper.vm.displayNonServiceableZipAlert).toBe(true);
expect(wrapper.findComponent({ ref: "alertNonServiceableZip" }).exists()).toBe(
true
);
expect(wrapper.findComponent({ ref: "alertNonServiceableZip" }).isVisible()).toBe(
true
);
});
test("if registration address, service zip are provided, and user clicks continue => don't update service address", async () => {
// Arrange
const mockRegistrationAddress = {
streetAddress: "1234 Main St",
city: "Columbus",
state: "OH",
zipCode: "43215",
};
const { wrapper } = setupMocks({
isZipServiceable: false,
});
store.commit(storeMutations.UPDATE_CAR_ID, "CARID");
await wrapper.setData({
customerQuestions: {
addressQuestions: mockRegistrationAddress,
},
});
// Act
await wrapper.vm.forwardButtonAction();
//FIX THIS
// Assert
expect(wrapper.vm.dispatchStoreAction).not.toHaveBeenCalledWith(
storeActions.UPDATE_SERVICE_LOCATION_WITH_VEHICLE_REGISTRATION
);
});
test("if registration address, service zip are provided, and user clicks continue => both zips are saved and are different", async () => {
// Arrange
const mockRegistrationAddress = {
streetAddress: "1234 Main St",
city: "Columbus",
state: "OH",
zipCode: "43215",
};
const { wrapper } = setupMocks({});
wrapper.vm.navigateForwardWithSingleCarMatch = jest.fn();
store.commit(storeMutations.UPDATE_CAR_ID, "CARID");
wrapper.vm.dispatchStoreAction = jest.fn();
wrapper.vm.dispatchStoreAction.mockImplementation((actionName, value) => {
let data = {};
if (actionName == storeActions.VALIDATE_ZIP) {
if (value == "43215") {
data = {
isServiceable: false,
};
} else {
data = {
isServiceable: true,
};
}
} else if (actionName == storeActions.LOOKUP_VIN_BY_ADDRESS) {
data = {
isStatePermissible: true,
vinVehicles: [
{
vin: "TEST_VIN",
vehicle: {
carId: "CARID",
},
},
],
};
}
return Promise.resolve({ data });
});
await wrapper.setData({
customerQuestions: {
addressQuestions: mockRegistrationAddress,
},
});
await wrapper.vm.forwardButtonAction();
await wrapper.setData({
serviceZipCode: "12345",
});
// // Act
await wrapper.vm.forwardButtonAction();
// // Assert
expect(wrapper.vm.$store.getters.order.serviceLocation.zipCode).not.toEqual(
wrapper.vm.$store.getters.vehicle.registration.zipCode
);
expect(wrapper.vm.$store.getters.vehicle.registration.zipCode).toEqual("12345");
expect(wrapper.vm.$store.getters.order.serviceLocation.zipCode).toEqual("11111");
});
});
});
});
function setupMocks({
isZipValid = true,
isZipServiceable = true,
lookupVinbyAddressResponse,
partsOrQuestions = [],
isStatePermissible = true,
vinVehicles = [],
carId = "C0000",
}) {
store.commit(storeMutations.RESET_STATE);
const wrapper = shallowMount(
addressLookup,
getMountOptions({
actionList: [
{
actionName: storeActions.VALIDATE_ZIP,
data: {
isValid: isZipValid,
isServiceable: isZipServiceable,
},
},
{
actionName: storeActions.LOOKUP_VIN_BY_ADDRESS,
data: lookupVinbyAddressResponse
? lookupVinbyAddressResponse
: {
isStatePermissible: true,
vinVehicles: [
{
vin: "TEST_VIN",
vehicle: {
carId: "CARID",
},
},
],
},
},
{
actionName: storeActions.GET_PARTS_OR_QUESTIONS,
data: {
partsOrQuestions: partsOrQuestions,
},
},
],
router: {
navigate: jest.fn(),
navigate: jest.fn(),
navigateWithSaving: jest.fn(),
navigateWithoutSaving: jest.fn(),
},
store: {
getters: {
vehicle: {
carId: carId,
registration: {
licensePlate: "TESTPLATE",
zipCode: "12345",
},
},
order: {
customer: {
emailAddress: "test@test.com",
},
serviceLocation: {
zipCode: "11111",
},
},
},
},
})
);
const apiResponses = {
serviceZipValidationResponse: {
isValid: isZipValid,
isServiceable: isZipServiceable,
},
vinLookupResponse: {
isStatePermissible: isStatePermissible,
vinVehicles: vinVehicles,
},
};
settleAllPromises.mockImplementation(() => apiResponses);
wrapper.vm.getCmsContent = jest.fn().mockImplementation(() => "");
wrapper.vm.setCmsContent = jest.fn();
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn();
return { wrapper };
}

View file

@ -1,90 +1,465 @@
<template>
<Form @submit="onSubmit" @invalid-submit="onInvalidSubmit">
<div class="page-container-grouped-styles overflow-auto">
<SiteHeader
cms-widget-name="SiteHeaderWidget"
/>
<VehicleBanner
cms-widget-name="VehicleBannerWidget"
:display-generic-vehicle-image="false"
/>
<SiteSubHeader
cms-widget-name="SiteSubHeaderWidget"
/>
<div class="fade-on-route-transition sub-container make-tall">
<p>Address Lookup Page Placeholder</p>
<SiteFooter
cms-widget-name="SiteFooterWidget"
:is-forward-action-disabled="isForwardActionDisabled"
@back-clicked="backButtonAction"
@forward-clicked="forwardButtonAction"
/>
</div>
</div>
</Form>
<template>
<Form
@submit="onSubmit"
@invalid-submit="onInvalidSubmit"
ref="theForm"
v-slot="{ meta }"
autocomplete="off">
<div class="page-container-grouped-styles">
<siteHeader cmsWidgetName="siteHeaderWidget" ref="siteHeader" />
<vehicleBanner
cmsWidgetName="VehicleBannerWidget"
ref="vehicleBanner"
:displayGenericVehicleImage="false" />
<siteSubHeader cmsWidgetName="siteSubHeaderWidget" ref="siteSubHeader" />
<div class="fade-on-route-transition sub-container make-tall">
<customerQuestions ref="customerQuestions" v-model="customerQuestions" />
<alert
ref="alertVinNotFound"
v-if="displayVinNotFoundAlert"
class="mb-4"
cmsWidgetName="AlertVinNotFoundWidget"
alertClass="alert-danger"
v-bind:isDismissible="false" />
<alert
ref="alertMatchedDifferentVehicle"
v-if="displayMatchedDifferentVehicleAlert"
class="mb-4"
:manualHeadline="AlertMatchedDifferentVehicleHeader"
:manualCopy="AlertMatchedDifferentVehicleBody"
alertClass="alert-warning"
v-bind:isDismissible="false" />
<alert
ref="alertInvalidZip"
v-if="displayInvalidZipAlert"
class="mb-4"
cmsWidgetName="AlertInvalidZipWidget"
alertClass="alert-danger"
v-bind:isDismissible="false" />
<alert
ref="alertNonServiceableZip"
v-if="displayNonServiceableZipAlert"
class="mb-4"
alertClass="alert-danger"
:manualHeadline="AlertNonServiceableZipHeader"
:manualCopy="AlertNonServiceableZipBody"
v-bind:isDismissible="false" />
<alert
ref="alertVinLookupsByHomeAddressNotAllowed"
v-if="displayVinLookupByHomeAddressNotAllowedAlert"
class="mb-4"
cmsWidgetName="AlertVinLookupsByHomeAddressNotAllowedWidget"
alertClass="alert-danger"
v-bind:isDismissible="false" />
<transition name="fade" mode="out-in">
<div class="service-zip-field" v-if="showServiceZipField" aria-live="polite">
<div class="row mb-4">
<div class="col">
<textboxQuestion
cmsWidgetName="ServiceZipQuestionWidget"
v-model="serviceZipCode"
ref="serviceZip"
inputId="7add1b26df344f2caf1678de5797803f"
aria-haspopup=""
mask="#####"
validationRules="service-zip-required|service-zip-format" />
</div>
</div>
</div>
</transition>
<siteFooter
cmsWidgetName="siteFooterWidget"
ref="siteFooter"
:isDisabled="!meta.valid"
@ForwardClicked="forwardButtonAction"
@back-clicked="backButtonAction"
:isForwardActionDisabled="!meta.valid" />
</div>
</div>
</Form>
</template>
<script>
// Import Supporting Files
import { fetchCmsContentForPage } from '@/helpers/cms-content-helper';
import { settleAllPromises } from '@/helpers/layout-helper';
import { Form } from 'vee-validate';
import BaseFormMixin from '@/mixins/base-form-mixin.js';
// Import Component
import SiteFooter from '@/common-components/site-footer/site-footer.vue';
import SiteHeader from '@/common-components/site-header/site-header.vue';
import SiteSubHeader from '@/common-components/site-sub-header/site-sub-header.vue';
import VehicleBanner from '@/common-components/vehicle-banner/vehicle-banner.vue';
<script>
// Components
import siteHeader from "@/common-components/site-header/site-header.vue";
import siteFooter from "@/common-components/site-footer/site-footer";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import siteSubHeader from "@/common-components/site-sub-header/site-sub-header";
import customerQuestions from "@/layouts/address-lookup/customer-questions/customer-questions";
import alert from "@/ux-components/alert/alert";
import textboxQuestion from "@/common-components/textbox-question/textbox-question";
import { Form, defineRule } from "vee-validate";
import { required, regex } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
// Supporting files
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { settleAllPromises } from "@/helpers/layout-helper";
import { routerParams } from "@/router/router-constants/router-params";
import { getDamageString, isGlassAvailableForCarId } from "@/helpers/damage-helper";
import vinPagesMixin from "@/mixins/vin-pages-mixin";
import { useMainStore } from "@/store"
// DEFINE VALIDATION RULES - Note: Additional rules are defined in Customer Questions and Address Questions components
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: [BaseFormMixin],
components: {
Form,
SiteFooter,
SiteHeader,
SiteSubHeader,
VehicleBanner,
},
data() {
return {};
},
async beforeRouteEnter(to, from, next) {
const cmsContentPromise = fetchCmsContentForPage(to.query.issPage);
name: "address-lookup",
mixins: [vinPagesMixin],
async beforeRouteEnter(to, from, next) {
// Call APIs
const cmsContentPromise = fetchCmsContentForPage(to.query.issPage);
// Settle promises and get results
const promiseResultMap = [
{
resultKey: 'cmsContent',
promise: cmsContentPromise,
},
];
// Settle promises and get results
const promiseResultMap = [
{
resultKey: "cmsContent",
promise: cmsContentPromise,
},
];
const resultMap = await settleAllPromises(promiseResultMap);
const resultMap = await settleAllPromises(promiseResultMap);
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
});
},
computed: {
isForwardActionDisabled() {
return true;
}
},
methods: {
arePagePrerequisiteValid() {
return true;
// Call the "next" function to complete the transition to this page.
next((vm) => {
vm.setCmsContent(resultMap.cmsContent);
});
},
backButtonAction() {
/**
* this.navigationScenarios comes from base-mixin
*/
this.$router.navigate(this.navigationScenarios.CLICKED_BACK, this.$route);
data() {
return {
customerQuestions: {
addressQuestions: {
streetAddress: this.getRegistrationAddressFromStore(),
city: this.getRegistrationCityFromStore(),
state: this.getRegistrationStateFromStore(),
zipCode: this.getRegistrationZipFromStore(),
},
firstName: this.getRegistrationFirstNameFromStore(),
lastName: this.getRegistrationLastNameFromStore(),
emailAddress: this.getEmailFromStore(),
},
serviceZipCode: this.getServiceZipFromStore(),
displayNonServiceableZipAlert: false,
displayVinNotFoundAlert: false,
displayMatchedDifferentVehicleAlert: false,
displayVinLookupByHomeAddressNotAllowedAlert: false,
previouslyEnteredCarId: "",
isCarIdDifferent: false,
isSelectedGlassAvailableForVehicle: true,
customAlertData: {},
displayInvalidZipAlert: false,
showServiceZipField: this.getServiceZipFromStore(),
isZipServiceable: false,
};
},
forwardButtonAction() {
return true;
methods: {
arePagePrerequisitesValid() {
return this.mainStore.order.vehicle.carId !== null;
},
backButtonAction() {
// route to move backwards
this.$router.navigateWithoutSaving(this.navigationScenarios.CLICKED_BACK, this.$route);
},
attachCustomEvents() {
this.prependActionToMethod(this, this.forwardButtonAction, () => {
this.pushEventToGA(
this.$route.query[this.queryStrings.ISS_PAGE],
this.GaActions.SUBMITTED,
this.GaLabels.ADDRESS_LOOKUP,
true
);
});
},
getRegistrationAddressFromStore() {
return this.mainStore.order.vehicle.registration.address;
},
getRegistrationCityFromStore() {
return this.mainStore.order.vehicle.registration.city;
},
getRegistrationStateFromStore() {
return this.mainStore.order.vehicle.registration.state;
},
getRegistrationZipFromStore() {
return this.mainStore.order.vehicle.registration.zipCode;
},
getRegistrationFirstNameFromStore() {
return this.mainStore.order.vehicle.registration.firstName;
},
getRegistrationLastNameFromStore() {
return this.mainStore.order.vehicle.registration.lastName;
},
getEmailFromStore() {
return this.mainStore.order.customer.emailAddress;
},
getServiceZipFromStore() {
return this.mainStore.order.serviceLocation.zipCode;
},
async forwardButtonAction() {
this.resetWarningsAndErrors();
// Vehicle info change in the flow, use a variable to keep track and commit to state at the end.
let vehicleInfoToCommit = {};
//Todo: Look up VIN by address
/*
const vinLookupResponse = this.dispatchStoreAction(
storeActions.LOOKUP_VIN_BY_ADDRESS,
{
licenseLastName: this.customerQuestions.lastName,
licenseStreetAddress: this.customerQuestions.addressQuestions.streetAddress,
licenseZip: this.customerQuestions.addressQuestions.zipCode,
licenseState: this.customerQuestions.addressQuestions.state,
},
false
);
*/
// Settle promises and get results
const promiseResultMap = [
{
resultKey: "vinLookupResponse",
promise: vinLookupResponse,
},
{
resultKey: "serviceZipValidationResponse",
promise: this.serviceZipCode
? this.dispatchStoreAction(storeActions.VALIDATE_ZIP, {
zip: this.serviceZipCode,
})
: this.dispatchStoreAction(storeActions.VALIDATE_ZIP, {
zip: this.customerQuestions.addressQuestions.zipCode,
}),
},
];
const resultMap = await settleAllPromises(promiseResultMap);
// If VIN Lookup by address is forbidden by State Restrictions then show an alert
if (!resultMap.vinLookupResponse.isStatePermissible) {
// State Restrictions forbid lookup by address
this.displayVinLookupByHomeAddressNotAllowedAlert = true;
return this.$refs.siteFooter.removeLoader();
}
// If a Service Zip is entered and it is an invalid zip code (ex. 11111) then show an alert
const isZipValid = resultMap.serviceZipValidationResponse.isValid;
if (this.serviceZipCode && !isZipValid) {
this.displayInvalidZipAlert = true;
return this.$refs.siteFooter.removeLoader();
}
this.displayInvalidZipAlert = false;
const carsFound = resultMap.vinLookupResponse.vinVehicles;
// Handle cases for different amounts of VINS found for the address.
if (carsFound.length == 1) {
// Single VIN found
const carFound = carsFound[0].vehicle;
this.isCarIdDifferent = carFound.carId !== this.mainStore.order.vehicle.carId;
if (this.isCarIdDifferent && carFound.carId !== this.previouslyEnteredCarId) {
// Display Alert
this.previouslyEnteredCarId = carFound.carId;
this.customAlertData.vehicleInfo = carFound;
this.displayMatchedDifferentVehicleAlert = true;
this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId(
carFound.carId
);
// Update button "Continue with..."
this.$refs.siteFooter.updateButtonText(
`Continue with ${carFound.year} ${carFound.make} ${carFound.model}`
);
return this.$refs.siteFooter.removeLoader();
}
// update data if the zip or service zip is serviceable
vehicleInfoToCommit = Object.assign(carFound, { vin: carsFound[0].vin });
} else if (carsFound.length > 1) {
// If multiple cars were found and one and only one of them matches the carId entered, save the vehicle info
// so we can go to the Heritage Funnel directly
const matchingCars = carsFound.filter(
(vin) => vin.vehicle.carId === this.mainStore.order.vehicle.carId
);
if (matchingCars.length === 1) {
vehicleInfoToCommit = Object.assign(matchingCars[0].vehicle, {
vin: matchingCars[0].vin,
});
}
} else {
// No VINS found.
this.displayVinNotFoundAlert = true;
return this.$refs.siteFooter.removeLoader();
}
// If the either the registration zip code or service zip code are not serviceable
this.isZipServiceable = resultMap.serviceZipValidationResponse.isServiceable;
if (!this.isZipServiceable) {
this.displayNonServiceableZipAlert = true;
this.showServiceZipField = true;
return this.$refs.siteFooter.removeLoader();
}
// If the registration zip code is serviceable and nothing was entered for the service zip code
// then set the service zip code to the registration zip code
if (!this.serviceZipCode) {
this.serviceZipCode = this.customerQuestions.addressQuestions.zipCode;
}
// Save vehicle, customer, service and registration information
await this.dispatchStoreAction(
storeActions.SAVE_REGISTRATION_ADDRESS_LOOKUP,
{
isSelectedGlassAvailableForVehicle: this.isSelectedGlassAvailableForVehicle,
vehicleInfo:
Object.keys(vehicleInfoToCommit).length === 0
? this.mainStore.order.vehicle
: vehicleInfoToCommit,
registrationInfo: {
firstName: this.customerQuestions.firstName,
lastName: this.customerQuestions.lastName,
address: this.customerQuestions.addressQuestions.streetAddress,
city: this.customerQuestions.addressQuestions.city,
state: resultMap.serviceZipValidationResponse.state,
zipCode: this.customerQuestions.addressQuestions.zipCode,
},
},
false
);
await this.dispatchStoreAction(
storeActions.SAVE_EMAIL,
this.customerQuestions.emailAddress,
false
);
await this.dispatchStoreAction(
storeActions.SAVE_SERVICE_LOCATION,
{
address: this.customerQuestions.addressQuestions.streetAddress,
city: this.customerQuestions.addressQuestions.city,
zipCode: this.serviceZipCode,
state: resultMap.serviceZipValidationResponse.state,
zipCodeCtu: resultMap.serviceZipValidationResponse.zipCodeCtu,
},
false
);
return await this.navigateForward(carsFound);
},
async navigateForward(carsFound) {
// Match vehicles found to vehicles in state.
const matchingCars = carsFound.filter(
(car) => car.vehicle.carId === this.mainStore.order.vehicle.carId
);
// If a different vehicle is found than the one entered and the selected glass is not available for that vehicle then navigate back to "vehicle-damage"
// display vehicle changed alert on that page.
if (
this.isCarIdDifferent &&
!this.isSelectedGlassAvailableForVehicle &&
matchingCars.length === 1
) {
this.$router.navigateWithSaving(
this.navigationScenarios.SELECTED_VIN_WITH_MISMATCHED_GLASS,
this.$route,
{},
{ [routerParams.DISPLAY_VEHICLE_CHANGE_ALERT]: true }
);
} else if (matchingCars.length === 1) {
await this.navigateForwardWithSingleCarMatch();
} else {
this.$router.navigateWithSaving(
this.navigationScenarios.CLICKED_FORWARD_WITH_MULTIPLE_VEHICLES,
this.$route,
{},
{},
carsFound
);
}
},
resetWarningsAndErrors() {
this.displayVinNotFoundAlert = false;
this.displayNonServiceableZipAlert = false;
this.displayMatchedDifferentVehicleAlert = false;
this.displayVinLookupByHomeAddressNotAllowedAlert = false;
},
},
mounted() {
this.attachCustomEvents();
},
computed: {
AlertNonServiceableZipHeader() {
const zipCode = this.serviceZipCode
? this.serviceZipCode
: this.customerQuestions.addressQuestions.zipCode;
const text = this.getCmsContent(
"AlertNonServiceableZipWidget",
"HeadlineText"
).replaceAll("{custom:serviceZip}", zipCode);
return text;
},
AlertNonServiceableZipBody() {
return this.getCmsContent("AlertNonServiceableZipWidget", "BodyText");
},
AlertMatchedDifferentVehicleHeader() {
return this.getCmsContent(
"AlertMatchedDifferentVehicleWidget",
"HeadlineText"
).replaceAll("{custom:glassText}", getDamageString());
},
AlertMatchedDifferentVehicleBody() {
const vinYmmFound = `${this.customAlertData?.vehicleInfo?.year} ${this.customAlertData?.vehicleInfo?.make} ${this.customAlertData?.vehicleInfo?.model}`;
const vinYmmExpected = `${useMainStore().order.vehicle.year} ${useMainStore().order.vehicle.make} ${useMainStore().order.vehicle.model}`;
return this.getCmsContent("AlertMatchedDifferentVehicleWidget", "BodyText")
.replaceAll("{custom:glassText}", getDamageString())
.replaceAll("{custom:vinYmmFound}", vinYmmFound)
.replaceAll("{custom:vinYmmExpected}", vinYmmExpected);
},
},
watch: {
customerQuestions: {
handler(newValue) {
// if they modify one of the lookup fields (address, city, state, zipCode, or lastName), then modify the button text back to "Get my personalized quote"
this.$refs.siteFooter.updateButtonText(
this.getCmsContent("siteFooterWidget", "ForwardButtonText")
);
this.showServiceZipField = false;
this.resetWarningsAndErrors();
},
deep: true,
},
serviceZipCode: {
handler(newValue) {
// If they modify the service zip code, then hide the error message.
this.displayNonServiceableZipAlert = false;
},
},
showServiceZipField: {
handler(newValue) {
// If the Service Zip Code field is ever hidden, clear out it's value
if (!newValue) {
this.serviceZipCode = null;
}
},
},
},
components: {
siteHeader,
siteFooter,
vehicleBanner,
siteSubHeader,
customerQuestions,
textboxQuestion,
alert,
Form,
},
resetDependentState() {},
},
};
</script>

View file

@ -0,0 +1,590 @@
// Components
import addressQuestions from "@/layouts/address-lookup/customer-questions/address-questions/address-questions";
import alert from "@/ux-components/alert/alert";
// Supporting Files
import { mount, shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { storeMutations } from "@/constants/store-mutations";
import store from "@/store";
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);
});
describe("initial state", () => {
test("only street address field is shown", () => {
// Arrange
const { wrapper } = setupMocks({});
// 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" });
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);
});
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 autofill", 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
wrapper.vm.$nextTick(function () {
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, 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",
},
});
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 modifies street address => noMatch warning is removed", async () => {
// Arrange
const { wrapper } = setupMocks({});
await wrapper.setData({
matchFound: false,
});
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, {
streetAddress: "LS",
});
// Assert
wrapper.vm.$nextTick(function () {
expect(wrapper.vm.displayNoMatchWarning).toBeFalsy();
noMatchAlert = wrapper.findComponent({ ref: "alertNoMatchWarning" });
expect(noMatchAlert.exists()).toBeFalsy();
});
});
test("user sees noMatch warning and enters city => noMatch warning is removed", async () => {
// Arrange
const { wrapper } = setupMocks({});
await wrapper.setData({
matchFound: false,
});
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, {
city: "LS",
});
// Assert
wrapper.vm.$nextTick(function () {
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({});
await wrapper.setData({
matchFound: false,
});
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, {
state: "KO",
});
// Assert
wrapper.vm.$nextTick(function () {
expect(wrapper.vm.displayNoMatchWarning).toBeFalsy();
noMatchAlert = wrapper.findComponent({ ref: "alertNoMatchWarning" });
expect(noMatchAlert.exists()).toBeFalsy();
});
});
test("user sees noMatch warning and enters zip code => noMatch warning is removed", async () => {
// Arrange
const { wrapper } = setupMocks({});
await wrapper.setData({
matchFound: false,
});
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, {
zipCode: "12345",
});
// Assert
wrapper.vm.$nextTick(function () {
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);
const resultingMountOptions = getMountOptions({
...mountOptions,
router: {
navigate: jest.fn(),
navigate: jest.fn(),
},
loadScript: jest.fn().mockResolvedValue(),
});
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();
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

@ -0,0 +1,403 @@
<template>
<div role="application">
<div class="row mt-2 mb-4">
<div class="col">
<textboxQuestion
id="streetAddressField"
cmsWidgetName="StreetAddressQuestionWidget"
v-model="addressModel.streetAddress"
ref="autocomplete"
inputId="autocomplete"
placeholderText="Search"
aria-haspopup=""
hasIcon
disableAutoFill
validationRules="street-address-required"
@keydown.enter.prevent />
</div>
</div>
<transition name="fade" mode="out-in">
<div class="row mb-4" v-show="showAddressFields" aria-live="polite">
<div class="col">
<textboxQuestion
cmsWidgetName="CityQuestionWidget"
v-model="addressModel.city"
ref="city"
inputId="cbf28188fdf2436688fd735915f7ee56"
disableAutoFill
validationRules="city-required" />
</div>
</div>
</transition>
<transition name="fade" mode="out-in">
<div class="row mb-4" v-show="showAddressFields" aria-live="polite">
<div class="col">
<dropdownQuestion
cmsWidgetName="StateQuestionWidget"
v-model="addressModel.state"
ref="state"
inputId="8fdf9dc2e13e430eb57529499dceb3eb"
:options="stateOptions"
disableAutoFill
validationRules="state-required" />
</div>
<div class="col">
<textboxQuestion
cmsWidgetName="ZipQuestionWidget"
v-model="addressModel.zipCode"
ref="zipCode"
inputId="01a9a1c2de0b4c9da8e023c9ae3be498"
mask="#####"
disableAutoFill
validationRules="zip-code-required|zip-code-format" />
</div>
</div>
</transition>
<alert
ref="alertVerificationWarning"
v-if="displayVerificationWarning"
class="mb-4"
cmsWidgetName="AlertVerificationWarningWidget"
alertClass="alert-warning"
v-bind:isDismissible="false" />
<alert
ref="alertNoMatchWarning"
v-if="displayNoMatchWarning"
class="mb-4"
cmsWidgetName="AlertNoMatchWarningWidget"
alertClass="alert-warning"
v-bind:isDismissible="false" />
</div>
</template>
<script>
import textboxQuestion from "@/common-components/textbox-question/textbox-question";
import dropdownQuestion from "@/common-components/dropdown-question/dropdown-question";
import alert from "@/ux-components/alert/alert";
import { applicationConfig } from "@/constants/application-config.js";
import { defineRule } from "vee-validate";
import { required, regex } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
// DEFINE VALIDATION RULES
defineRule("street-address-required", required(errorMessages.STREET_ADDRESS_REQUIRED));
defineRule("city-required", required(errorMessages.CITY_REQUIRED));
defineRule("state-required", required(errorMessages.STATE_REQUIRED));
defineRule("zip-code-required", required(errorMessages.ZIP_REQUIRED));
defineRule("zip-code-format", regex(/(^\d{5}$)|(^\d{5}-\d{4}$)/, errorMessages.ZIP_FORMAT));
export default {
name: "address-questions",
emits: ["update:modelValue"], // The component emits an event
props: {
modelValue: {
type: Object,
default: () => ({
streetAddress: "",
city: "",
state: "",
zipCode: "",
}),
},
validationRules: String,
},
data() {
return {
showAddressFields: false,
displayVerificationWarning: false,
displayNoMatchWarning: false,
alertHeadlineVerificationWarning: "",
alertCopyVerificationWarning: "",
alertHeadlineNoMatchWarning: "",
alertCopyNoMatchWarning: "",
matchingIndirectly: false,
matchFound: null, // null = no attempted match, true = match was found, false = match was not found
enterPressed: false,
isAddressWatchActive: false, // Only deep watch the address model when a match was not found
};
},
computed: {
stateOptions: {
get: function () {
return {
AL: "Alabama",
AK: "Alaska",
AZ: "Arizona",
AR: "Arkansas",
CA: "California",
CO: "Colorado",
CT: "Connecticut",
DE: "Delaware",
DC: "District Of Columbia",
FL: "Florida",
GA: "Georgia",
HI: "Hawaii",
ID: "Idaho",
IL: "Illinois",
IN: "Indiana",
IA: "Iowa",
KS: "Kansas",
KY: "Kentucky",
LA: "Louisiana",
ME: "Maine",
MD: "Maryland",
MA: "Massachusetts",
MI: "Michigan",
MN: "Minnesota",
MS: "Mississippi",
MO: "Missouri",
MT: "Montana",
NE: "Nebraska",
NV: "Nevada",
NH: "New Hampshire",
NJ: "New Jersey",
NM: "New Mexico",
NY: "New York",
NC: "North Carolina",
ND: "North Dakota",
OH: "Ohio",
OK: "Oklahoma",
OR: "Oregon",
PA: "Pennsylvania",
RI: "Rhode Island",
SC: "South Carolina",
SD: "South Dakota",
TN: "Tennessee",
TX: "Texas",
UT: "Utah",
VT: "Vermont",
VA: "Virginia",
WA: "Washington",
WV: "West Virginia",
WI: "Wisconsin",
WY: "Wyoming",
};
},
},
addressModel: {
get: function () {
return this.modelValue;
},
set: function (newValue) {
this.$emit("update:modelValue", newValue);
},
},
},
methods: {
setupAddressLookup() {
if (
this.addressModel.streetAddress &&
this.addressModel.city &&
this.addressModel.state &&
this.addressModel.zipCode
) {
this.showAddressFields = true;
return;
}
const addressField1 = document.getElementById("autocomplete");
const self = this;
const apiKey = applicationConfig.GOOGLE_PLACES_API_KEY;
this.$loadScript(
`https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places`
)
.then(() => {
// Script is loaded, initialize the autocomplete textbox
const autocomplete = new window.google.maps.places.Autocomplete(addressField1, {
componentRestrictions: { country: ["us"] },
fields: ["address_components"],
types: ["geocode"],
});
// Standard place_changed event handling
const autocompleteListener = window.google.maps.event.addListener(
autocomplete,
"place_changed",
fillInAddress
);
addressField1.addEventListener("focus", () => {
// Wrapping the addressField1 element in the Google Address Autocomplete object
// will cause "autocomplete='off'" which Chrome completely ignores. This event
// handler will set the value to something arbitrary so autofill doesn't work.
// https://stackoverflow.com/a/30976223
addressField1.setAttribute("autocomplete", "do-not-autofill");
// Make place results box stick to the input on scroll
const streetAddressField = document.getElementById("streetAddressField");
const autocompleteResultsContainer =
document.getElementsByClassName("pac-container")[0];
if (autocompleteResultsContainer) {
streetAddressField.appendChild(autocompleteResultsContainer);
}
});
addressField1.addEventListener("keydown", (e) => {
if (e.code === "Enter" || e.code === "NumpadEnter" || e.code === "Tab") {
if (e.code === "Tab") {
self.matchingIndirectly = true;
} else {
self.enterPressed = true;
}
addressField1.blur();
} else {
return;
}
});
addressField1.addEventListener("change", () => {
// NOTE: The "place_changed" event of the autocomplete fires after this and will use either the address the user had chosen
// using either the down / up arrows or the address the user was hovering over when they pressed "Enter."
// If a match has been previously found then do nothing
// OR
// If the user pressed "Enter" then do nothing
if (self.matchFound || self.enterPressed) {
return;
}
// Get the address that the user clicked on (if any)
const clickedAddress = document.querySelector(
".pac-container .pac-item:hover"
);
// If the Street Address field changed without clicking (i.e. by pressing Tab, or clicking outside the field)
if (clickedAddress === null) {
// Fill-in the address using first item in the list.
const item = document.querySelector(".pac-container .pac-item");
if (item != null) {
self.matchingIndirectly = true;
const firstResult = item.textContent;
const geocoder = new window.google.maps.Geocoder();
geocoder.geocode(
{
address: firstResult,
},
function (results, status) {
if (status === window.google.maps.GeocoderStatus.OK) {
fillInAddress(results[0]);
}
}
);
} else {
// No addresses found for the input
self.matchFound = false;
}
}
});
function fillInAddress(place) {
if (!place) {
place = autocomplete.getPlace();
}
if (place && place.address_components) {
self.matchFound = true;
self.addressModel.streetAddress = "";
self.$nextTick(function () {
self.showAddressFields = true;
for (const component of place.address_components) {
const componentType = component.types[0];
switch (componentType) {
case "street_number": {
self.addressModel.streetAddress = component.long_name;
break;
}
case "route": {
self.addressModel.streetAddress +=
" " + component.short_name;
break;
}
case "locality": {
self.addressModel.city = component.long_name;
break;
}
case "administrative_area_level_1": {
self.addressModel.state = component.short_name;
break;
}
case "postal_code": {
self.addressModel.zipCode = component.long_name;
break;
}
}
}
self.displayVerificationWarning = self.matchingIndirectly;
// after showing the address fields, disable the address autocomplete
window.google.maps.event.removeListener(autocompleteListener);
window.google.maps.event.clearInstanceListeners(autocomplete);
addressField1.onchange = null;
const pacContainer = document.querySelector(".pac-container");
if (pacContainer) {
pacContainer.remove();
}
});
} else {
self.displayVerificationWarning = true;
}
}
})
.catch(() => {
// Failed to fetch script
console.log("Unable to load Google Places API script");
});
},
},
mounted() {
this.setupAddressLookup();
},
watch: {
matchFound: {
handler(newValue) {
if (!newValue) {
this.displayNoMatchWarning = true;
this.addressModel.city = "";
this.addressModel.state = "";
this.addressModel.zipCode = "";
this.showAddressFields = true;
this.displayVerificationWarning = false;
this.$nextTick(function () {
// Only deep watch the Address Model after a failed match
this.isAddressWatchActive = true;
});
}
},
},
addressModel: {
handler() {
if (this.isAddressWatchActive) {
this.displayNoMatchWarning = false;
this.isAddressWatchActive = false;
}
},
deep: true,
},
},
components: {
textboxQuestion,
dropdownQuestion,
alert,
},
};
</script>
<style lang="scss">
#streetAddressField {
position: relative;
.pac-container {
top: 76px !important; // Height of #streetAddressField
left: 0 !important;
}
}
</style>

View file

@ -0,0 +1,33 @@
import { shallowMount } from "@vue/test-utils";
import customerQuestions from "@/layouts/address-lookup/customer-questions/customer-questions";
const customerModel = {
addressQuestions: {
streetAddress: "",
city: "",
state: "",
zipCode: "",
},
firstName: "",
lastName: "",
emailAddress: "",
};
describe("customerQuestions.vue", () => {
it("Should render customerQuestions sub-components (addressQuestions, first name, last name, and email textbox-questions)", async () => {
// Arrange
const wrapper = shallowMount(customerQuestions);
// Act
const addressQuestions = wrapper.findComponent({ ref: "addressQuestions" });
const firstName = wrapper.findComponent({ ref: "firstName" });
const lastName = wrapper.findComponent({ ref: "lastName" });
const emailAddress = wrapper.findComponent({ ref: "emailAddress" });
// Assert
expect(addressQuestions.exists()).toBe(true);
expect(firstName.exists()).toBe(true);
expect(lastName.exists()).toBe(true);
expect(emailAddress.exists()).toBe(true);
});
});

View file

@ -0,0 +1,102 @@
<template>
<addressQuestions ref="addressQuestions" v-model="customerModel.addressQuestions" />
<div class="row mb-4">
<div class="col">
<textboxQuestion
cmsWidgetName="FirstNameQuestionWidget"
v-model="customerModel.firstName"
ref="firstName"
inputId="08497a2efd9a4a73a70360ab47b4838d"
disableAutoFill
validationRules="first-name-required" />
</div>
</div>
<div class="row mb-4">
<div class="col">
<textboxQuestion
cmsWidgetName="LastNameQuestionWidget"
v-model="customerModel.lastName"
ref="lastName"
inputId="0030e56a57e74a4ab92de7fb8e97fec5"
disableAutoFill
validationRules="last-name-required" />
</div>
</div>
<div class="row mt-4">
<div class="col">
<textboxQuestion
cmsWidgetName="EmailAddressQuestionWidget"
v-model="customerModel.emailAddress"
ref="emailAddress"
inputId="00450a91b8964a768ce3992e6feb890f"
disableAutoFill
validationRules="email-address-required|email-address-format" />
</div>
</div>
<div class="row mb-4">
<div class="col">
<textBlock cmsWidgetName="QuoteEmailTextBlockWidget" typeStyle="caption" />
</div>
</div>
</template>
<script>
import addressQuestions from "@/layouts/address-lookup/customer-questions/address-questions/address-questions";
import textboxQuestion from "@/common-components/textbox-question/textbox-question";
import { defineRule } from "vee-validate";
import { required } from "@/helpers/validation-rules";
import { regex } from "@/helpers/validation-rules";
import { errorMessages } from "@/constants/error-messages";
import textBlock from "@/common-components/text-block/text-block";
// DEFINE VALIDATION RULES
defineRule("first-name-required", required(errorMessages.FIRST_NAME_REQUIRED));
defineRule("last-name-required", required(errorMessages.LAST_NAME_REQUIRED));
defineRule("email-address-required", required(errorMessages.EMAIL_ADDRESS_REQUIRED));
defineRule(
"email-address-format",
regex(
/^([a-zA-Z0-9_\-.+]+)@([a-zA-Z0-9_\-.]+)\.([a-zA-Z]{2,})$/,
errorMessages.EMAIL_ADDRESS_FORMAT
)
);
export default {
name: "customer-questions",
emits: ["update:modelValue"], // The component emits an event
props: {
modelValue: {
type: Object,
default: () => ({
customerQuestions: {
addressQuestions: {
streetAddress: "",
city: "",
state: "",
zipCode: "",
},
firstName: "",
lastName: "",
emailAddress: "",
},
}),
},
validationRules: String,
},
computed: {
customerModel: {
get: function () {
return this.modelValue;
},
set: function (newValue) {
this.$emit("update:modelValue", newValue);
},
},
},
components: {
addressQuestions,
textboxQuestion,
textBlock,
},
};
</script>

View file

@ -0,0 +1,15 @@
import { useMainStore } from "@/store";
import vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin";
export default {
methods: {
async navigateForwardWithSingleCarMatch() {
const store = useMainStore();
const result = await store.getParts();
const partsOrQuestions = result.data.partsOrQuestions;
vehicleQuestionsMixin.methods.navigateForward(partsOrQuestions, this);
},
},
};

View file

@ -0,0 +1,70 @@
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 vehicleQuestionsMixin from "@/mixins/vehicle-questions-mixin";
jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({
navigateForward: jest.fn(),
}));
jest.mock("@/helpers/heritage-integration/order-helper.js", () => ({
saveSession: jest.fn(),
}));
describe("vin-pages-mixin", () => {
afterEach(() => {
jest.clearAllMocks();
});
describe("navigateForwardWithSingleCarMatch", () => {
test("should navigateForward", async () => {
// Arrange
const { wrapper } = setupMocks({});
vehicleQuestionsMixin.methods.navigateForward = jest.fn();
// Act
await wrapper.vm.navigateForwardWithSingleCarMatch();
// Assert
expect(vehicleQuestionsMixin.methods.navigateForward).toHaveBeenCalled();
});
});
});
function setupMocks({ partsOrQuestions = [] }) {
const baseMixin = setupMocksForJsFiles({
actionList: [
{
actionName: storeActions.GET_PARTS_OR_QUESTIONS,
data: {
partsOrQuestions: partsOrQuestions,
},
},
],
});
const mocks = getMountOptions({
router: {
navigate: jest.fn(),
navigateWithSaving: jest.fn(),
navigateWithoutSaving: jest.fn(),
},
store: {
commit: jest.fn(),
getters: {
applicationUser: {
savedSessionId: 1,
},
},
},
});
const mockVinComponent = {
mixins: [vinPagesMixin, baseMixin.baseMixin],
};
const wrapper = shallowMount(mockVinComponent, mocks);
return { wrapper };
}

View file

@ -41,6 +41,9 @@ const getDefaultState = () => {
moldingQuestionAnswers: null,
capabilityQuestionAnswers: null,
},
customer: {
emailAddress: null,
},
serviceLocation: {
address: null,
city: null,

View file

@ -1,5 +1,7 @@
process.env.VUE_APP_CONSUMER_CF_DISTRO ="https://digitalapi.dev.safelite.io";
process.env.VUE_APP_CURRENT_ENVIRONMENT = "Localhost";
process.env.VUE_APP_GOOGLE_PLACES_API_KEY =
"AIzaSyDptGCkOPgN2uWJOy4ou4M33phRD4MAoJo";
// GA & GTM