Merge branch 'release/2022.06.23' into feature/CSR-480

This commit is contained in:
Adam Caouette 2022-06-13 20:51:41 -04:00
commit f1380cfe2f
32 changed files with 1946 additions and 430 deletions

View file

@ -133,7 +133,7 @@ stages:
# Prod Build/Deploy
- stage: Prod
condition: eq(variables['Build.SourceBranch'], variables['prod-branch'] )
condition: succeeded('Qa')
variables:
- group: FixMyGlassProd
jobs:
@ -172,11 +172,7 @@ stages:
__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__: $(__VUE_APP_GOOGLE_TAG_MANAGER_SCRIPT_BODY__)
__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__: $(__VUE_APP_GOOGLE_TAG_MANAGER_NOSCRIPT_FRAME_SRC__)
cfDistributionId: $(cfDistributionId)
- stage: AutoTagReleaseBuild
condition: eq(variables['Build.SourceBranch'], variables['prod-branch'] )
displayName: Auto Tag Release Build For Production
jobs:
- template: templates/digital/auto-tag.yml@AzureDevOps
parameters:
userName: SafeliteAzureDevops
userEmail: githubazuredevops@safelite.com
- template: templates/digital/auto-tag.yml@AzureDevOps
parameters:
userName: SafeliteAzureDevops
userEmail: githubazuredevops@safelite.com

View file

@ -18,14 +18,12 @@ module.exports = {
"!src/layouts/vehicle-damage/windshield-options/windshield-options.vue",
"!src/layouts/part-questions/**/*.vue",
"!src/layouts/reveal/**/*.vue",
"!src/layouts/estimate/**/*.vue",
// TODO REMOVE THESE AFTER WRITING UNIT TESTS
"!src/layouts/address-lookup/address-lookup.vue",
"!src/layouts/address-lookup/customer-questions/customer-questions.vue",
"!src/layouts/address-lookup/customer-questions/address-questions/address-questions.vue",
"!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/helpers/validation-rules.js",
"!src/common-components/menu-modal/menu-modal.vue",
"!src/common-components/question-chain/question-chain",
// 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

@ -1,4 +1,5 @@
<template>
<div class="row" :style="`padding-bottom: ${paddingHeight}px`"></div>
<footer class="footer container-fluid fixed-bottom g-5 bg-light py-4" id="infoBox">
<div class="row d-flex flex-row-reverse align-items-center vw-100">
<div class="col button-col d-flex" id="stacked">

View file

@ -20,7 +20,7 @@
import alert from "@/ux-components/alert/alert";
import eventBus from "@/helpers/event-bus/event-bus";
import { globalEvents } from "@/constants/events";
import menuModal from "@/common-components/menu-modal/menu-modal";
import menuModal from "@/common-components/funnel-header/menu-modal/menu-modal";
export default {
name: "funnel-header",

View file

@ -7,16 +7,16 @@
</button>
</div>
<!-- Modal -->
<div class="modal fade" id="footerModal" tabindex="-1" aria-labelledby="footerModalLabel" aria-hidden="true" :style="`height: calc(100% - ${currentFooterAndHeaderHeight}px);`">
<div class="modal fade" id="footerModal" tabindex="-1" aria-labelledby="footerModalLabel" aria-hidden="true" v-on="{ 'show.bs.modal' : show, 'hide.bs.modal' : hide }" :style="`height: calc(100% - ${currentFooterAndHeaderHeight}px);`">
<div class="modal-dialog modal-fullscreen">
<div class="modal-content">
<div class="modal-header visually-hidden">
<h5 class="modal-title" id="footerModalLabel">Footer Navigation</h5>
</div>
<div class="modal-body d-flex flex-column">
<textLink linkType="navigation" text="Terms of use" href="https://www.safelite.com/terms-of-use" />
<textLink linkType="navigation" text="Privacy policy" href="https://www.safelite.com/safelite-group-privacy-policy" />
<textLink linkType="navigation" text="Do not sell my information" href="https://privacyportal-cdn.onetrust.com/dsarwebform/d3b95a93-e22e-4d4d-a806-482052406557/9e371601-eae3-4338-9430-b90b9036022b.html" />
<textLink linkType="navigation" text="Terms of use" href="https://www.safelite.com/terms-of-use" target="_blank" />
<textLink linkType="navigation" text="Privacy policy" href="https://www.safelite.com/safelite-group-privacy-policy" target="_blank" />
<textLink linkType="navigation" text="Do not sell my information" href="https://privacyportal-cdn.onetrust.com/dsarwebform/d3b95a93-e22e-4d4d-a806-482052406557/9e371601-eae3-4338-9430-b90b9036022b.html" target="_blank" />
</div>
<div class="modal-footer d-flex justify-content-start">
&copy; {{new Date().getFullYear()}} Safelite Group
@ -36,16 +36,18 @@ export default {
currentFooterAndHeaderHeight: 0,
};
},
mounted() {
const myModalEl = document.getElementById('footerModal');
var self = this;
myModalEl.addEventListener('hide.bs.modal', function (event) {
methods: {
show() {
this.currentFooterAndHeaderHeight = this.getFooterInfoBoxHeight() + 72;
this.isActive = true;
document.querySelector('.page-container-grouped-styles').scrollTo({
top: 0, behavior: 'smooth'
});
},
hide() {
var self = this;
self.isActive = false;
});
myModalEl.addEventListener('show.bs.modal', function (event) {
self.currentFooterAndHeaderHeight = self.getFooterInfoBoxHeight() + 72;
self.isActive = true;
})
},
},
components: {
textLink,
@ -71,13 +73,14 @@ export default {
flex-direction: column;
justify-content: center;
align-items: center;
padding: 0;//Required to prevent 'squish' on iPhone
z-index: 1056;
.bar1,
.bar2,
.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

@ -1,7 +1,7 @@
const dynamicStrings = {
GLOBAL_STATE: "globalState",
CUSTOM: "custom",
ROUTER_LINK: "routerLink"
ROUTER_LINK: "routerLink:"
};
export { dynamicStrings };

View file

@ -120,4 +120,37 @@ function processWidgetItemForReplacement(widgetModel, key) {
// If we have something else like a number, boolean, etc. just return it
return widgetModel[key];
}
}
export function doesCopyContainRouterLink(copy) {
return copy.includes(this.dynamicStrings.ROUTER_LINK);
}
export function splitCopyOnCMSPlaceHolder(copy){
// splits copy on { ... } such as {routerlink: ...}
return copy.split(/{(.*?)}/g);
}
export function getRouterLinkRouteFromCopy(copy){
// sample input: {routerLink:estimate,provide your VIN}
// first split would return 'estimate,provide your VIN'
// second split would return 'estimate'
return copy.split(':')[1].split(',')[0];
}
export function getRouterLinkDisplayTextFromCopy(copy){
// sample input: {routerLink:estimate,provide your VIN}
// first split would return 'estimate,provide your VIN'
// second split would return 'provide your VIN'
return copy.split(':')[1].split(',')[1];
}
// Copy returned from the CMS that has newlines will return blocks wrapped in
// <p ... >...</p>
// This function returns an array of each paragraph, works with or without html
// attributes present
export function splitCMSCopyOnParagraphTag(copy) {
// filter removes empty strings that are a result of string.split with regex
return copy.split(/(?:<p(?:.*?)>)|(?:<\/p>)/g).filter(paragraph => paragraph !== "");
}

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

@ -1,38 +1,16 @@
// Components
import addressLookup from "@/layouts/address-lookup/address-lookup.vue";
import funnelHeader from "@/common-components/funnel-header/funnel-header";
import funnelFooter from "@/common-components/funnel-footer/funnel-footer";
import vehicleBanner from "@/common-components/vehicle-banner/vehicle-banner";
import funnelSubHeader from "@/common-components/funnel-sub-header/funnel-sub-header";
import customerQuestions from "@/layouts/address-lookup/customer-questions/customer-questions";
import addressQuestions from "@/layouts/address-lookup/customer-questions/address-questions/address-questions";
import router from "@/router";
// Supporting Files
import { settleAllPromises } from "@/helpers/layout-helper.js";
import baseMixin from "@/mixins/base-mixin";
import { fetchCmsContentForPage } from "@/helpers/cms-content-helper";
import { mount, flushPromises, shallowMount } from "@vue/test-utils";
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import { maska } from 'maska';
import { nextTick } from "vue";
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 { validate } from "vee-validate";
import { isGlassAvailableForCarId } from "@/helpers/damage-helper.js"
import { navigateAfterSaveToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { EXPECTATION_FAILED } from "http-status-codes";
// 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(),
// }));
jest.mock("@/helpers/damage-helper", () => ({
isGlassAvailableForCarId: jest.fn().mockImplementation(() => true),
@ -43,17 +21,530 @@ jest.mock("@/helpers/heritage-integration/navigation-helper", () => ({
navigateAfterSaveToHeritageFunnel: jest.fn()
}));
// const questionText = "Question Text";
// const mockMixin = {
// methods: {
// getCmsContent: jest.fn().mockImplementation(() => {
// return questionText;
// })
// }
// }
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(addressLookup, {
isZipServiceable: false
});
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(addressLookup, {
isZipServiceable: true
});
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(addressLookup, {
isZipServiceable: true
});
store.commit(storeMutations.UPDATE_CAR_ID, "CARID");
baseMixin.methods.dispatchStoreAction = jest.fn();
baseMixin.methods.dispatchStoreAction.mockImplementation((actionName, value) => {
let data = {};
if (actionName == storeActions.VALIDATE_ZIP) {
data = {
isServiceable: true
}
}
else if (actionName == storeActions.LOOKUP_VIN_BY_ADDRESS) {
data = {
isStatePermissible: false,
vinVehicles: [{
vin: "TEST_VIN",
vehicle: {
carId: "CARID"
}
},
{
vin: "TEST_VIN2",
vehicle: {
carId: "CARID2"
}
}]
}
}
return Promise.resolve({ data });
})
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(addressLookup, {
isZipServiceable: true
});
store.commit(storeMutations.UPDATE_CAR_ID, "CARID");
baseMixin.methods.dispatchStoreAction = jest.fn();
baseMixin.methods.dispatchStoreAction.mockImplementation((actionName, value) => {
let data = {};
if (actionName == storeActions.VALIDATE_ZIP) {
data = {
isServiceable: true
}
}
else if (actionName == storeActions.LOOKUP_VIN_BY_ADDRESS) {
data = {
isStatePermissible: true,
vinVehicles: [] // Return no vehicles
}
}
return Promise.resolve({ data });
})
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(addressLookup, {
isZipServiceable: true
});
// Act
await wrapper.vm.backButtonAction();
// Assert
expect(wrapper.vm.$router.navigate).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(addressLookup, {
isZipServiceable: true
});
store.commit(storeMutations.UPDATE_CAR_ID, "CARID");
baseMixin.methods.dispatchStoreAction = jest.fn();
baseMixin.methods.dispatchStoreAction.mockImplementation((actionName, value) => {
let data = {};
if (actionName == storeActions.VALIDATE_ZIP) {
data = {
isServiceable: true
}
}
else if (actionName == storeActions.LOOKUP_VIN_BY_ADDRESS) {
data = {
isStatePermissible: true,
vinVehicles: [{
vin: "TEST_VIN",
vehicle: {
carId: "CARID"
}
},
{
vin: "TEST_VIN2",
vehicle: {
carId: "CARID2"
}
}]
}
}
return Promise.resolve({ data });
})
await wrapper.setData({
customerQuestions: {
addressQuestions: mockRegistrationAddress
},
})
wrapper.vm.navigateForward = jest.fn();
// Act
await wrapper.vm.forwardButtonAction();
// 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
const mockRegistrationAddress = {
streetAddress: "1234 Main St",
city: "Columbus",
state: "OH",
zipCode: "43215"
}
const { wrapper } = setupMocks(addressLookup, {
isZipServiceable: true
});
store.commit(storeMutations.UPDATE_CAR_ID, "CARID");
baseMixin.methods.dispatchStoreAction = jest.fn();
baseMixin.methods.dispatchStoreAction.mockImplementation((actionName, value) => {
let data = {};
if (actionName == storeActions.VALIDATE_ZIP) {
data = {
isServiceable: true
}
}
else if (actionName == storeActions.LOOKUP_VIN_BY_ADDRESS) {
data = {
isStatePermissible: true,
vinVehicles: [{
vin: "TEST_VIN",
vehicle: {
carId: "CARID"
}
},
{
vin: "TEST_VIN2",
vehicle: {
carId: "CARID2"
}
}]
}
}
return Promise.resolve({ data });
})
await wrapper.setData({
customerQuestions: {
addressQuestions: mockRegistrationAddress
},
})
wrapper.vm.updateVehicleInfo = jest.fn();
// Act
await wrapper.vm.forwardButtonAction();
// 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
const mockRegistrationAddress = {
streetAddress: "1234 Main St",
city: "Columbus",
state: "OH",
zipCode: "43215"
}
const { wrapper } = setupMocks(addressLookup, {
isZipServiceable: true
});
store.commit(storeMutations.UPDATE_CAR_ID, "CARID_A");
baseMixin.methods.dispatchStoreAction = jest.fn();
baseMixin.methods.dispatchStoreAction.mockImplementation((actionName, value) => {
let data = {};
if (actionName == storeActions.VALIDATE_ZIP) {
data = {
isServiceable: true
}
}
else if (actionName == storeActions.LOOKUP_VIN_BY_ADDRESS) {
data = {
isStatePermissible: true,
vinVehicles: [{
vin: "TEST_VIN",
vehicle: {
carId: "CARID"
}
},
{
vin: "TEST_VIN2",
vehicle: {
carId: "CARID2"
}
}]
}
}
return Promise.resolve({ data });
})
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.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
const mockRegistrationAddress = {
streetAddress: "1234 Main St",
city: "Columbus",
state: "OH",
zipCode: "43215"
}
const { wrapper } = setupMocks(addressLookup, {
isZipServiceable: true
});
store.commit(storeMutations.UPDATE_CAR_ID, "CARID");
baseMixin.methods.dispatchStoreAction = jest.fn();
baseMixin.methods.dispatchStoreAction.mockImplementation((actionName, value) => {
let data = {};
if (actionName == storeActions.VALIDATE_ZIP) {
data = {
isServiceable: false
}
}
else if (actionName == storeActions.LOOKUP_VIN_BY_ADDRESS) {
data = {
isStatePermissible: true,
vinVehicles: [{
vin: "TEST_VIN",
vehicle: {
carId: "CARID"
}
},
{
vin: "TEST_VIN2",
vehicle: {
carId: "CARID2"
}
}]
}
}
return Promise.resolve({ data });
})
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(addressLookup, {
isZipServiceable: true
});
store.commit(storeMutations.UPDATE_CAR_ID, "CARID");
baseMixin.methods.dispatchStoreAction = jest.fn();
baseMixin.methods.dispatchStoreAction.mockImplementation((actionName, value) => {
let data = {};
if (actionName == storeActions.LOOKUP_VIN_BY_ADDRESS) {
data = {
isStatePermissible: true,
vinVehicles: [{
vin: "TEST_VIN",
vehicle: {
carId: "CARID2"
}
}]
}
}
return Promise.resolve({ data });
})
await wrapper.setData({
customerQuestions: {
addressQuestions: mockRegistrationAddress
},
isCarIdDifferent: true,
isGlassAvailableForCarId: false,
})
let carEntered = [{
vin: "TEST_VIN",
vehicle: {
carId: "CARID"
}
}];
let carsFound = [{
vin: "TEST_VIN2",
vehicle: {
carId: "CARID2"
}
}];
// Act
await wrapper.vm.navigateForward(carEntered, carsFound);
// Assert
expect(wrapper.vm.$router.navigateAfterSave).toHaveBeenCalledWith(navigationScenarios.CONTINUING_WITH_DIFFERENT_GLASS, undefined, {}, {"displayVehicleChangeAlert": true}, {});
});
});
describe("reseting 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");
const dispatchSpy = jest.spyOn(store, "dispatch");
const { wrapper } = setupMocks(addressLookup, {
isZipServiceable: true
});
// Act
wrapper.vm.resetDependentState();
// Assert
expect(commitSpy).toBeCalledWith(storeMutations.UPDATE_REGISTRATION_LICENSE_PLATE, null);
expect(dispatchSpy).toBeCalledWith(storeActions.RESET_PARTS_STATE_AND_DEPENDENCIES);
});
});
describe("registration and service zips", () => {
describe("if registration zip is serviceable", () => {
test("if registration address is provided => update service address on successful continue", async () => {
@ -242,77 +733,6 @@ describe("address-lookup.vue", () => {
});
});
});
describe.skip("initialization", () => {
test("Page header is initialized with api data", async (done) => {
//Arrange
const pageHeaderWidgetHeaderText = "Select Damage";
const { wrapper, apiPromise } = setupMocks({
pageHeaderWidgetHeaderText: pageHeaderWidgetHeaderText,
});
//Act
addressLookup.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "address-lookup" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
apiPromise.finally(() => {
expect(funnelSubHeader.methods.initializeComponent).toHaveBeenCalledWith(
pageHeaderWidgetHeaderText
);
done();
});
});
test("Customer Questions component is initialized with api data", async (done) => {
//Arrange
const StreetAddressQuestionWidget = { QuestionText: "test" };
const CityQuestionWidget = { QuestionText: "test" };
const StateQuestionWidget = { QuestionText: "test" };
const ZipQuestionWidget = { QuestionText: "test" };
const FirstNameQuestionWidget = { QuestionText: "test" };
const LastNameQuestionWidget = { QuestionText: "test" };
const EmailAddressQuestionWidget = { QuestionText: "test" };
const AlertVerificationWarningWidget = { HeaderText: "test", BodyText: "test" };
const AlertNoMatchWarningWidget = { HeaderText: "test", BodyText: "test" };
const widgets = [
StreetAddressQuestionWidget,
CityQuestionWidget,
StateQuestionWidget,
ZipQuestionWidget,
AlertVerificationWarningWidget,
AlertNoMatchWarningWidget,
FirstNameQuestionWidget,
LastNameQuestionWidget,
EmailAddressQuestionWidget,
];
const { wrapper, apiPromise } = setupMocks({
cmsContent: widgets,
});
//Act
addressLookup.beforeRouteEnter.call(
wrapper.vm,
{ query: { fmgPage: "address-lookup" } },
undefined,
(c) => c(wrapper.vm)
);
//Assert
apiPromise.finally(() => {
expect(customerQuestions.methods.initializeComponent).toHaveBeenCalledWith(
widgets
);
done();
});
});
});
});
function setupMocks(mountOptions, { isZipServiceable = true, lookupVinbyAddressResponse }) {
@ -351,154 +771,7 @@ function setupMocks(mountOptions, { isZipServiceable = true, lookupVinbyAddressR
wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn();
wrapper.vm.$refs.loadingModal.showModal = jest.fn();
// jest.mock("@/store", () => ({
// commit: jest.fn(),
// dispatch: jest.fn(),
// getters: {
// vehicle: {
// carId: "CARID"
// }
// },
// }));
return { wrapper };
}
// function setupMocks({
// pageHeaderWidgetHeaderText = {},
// mountOptionsMockData = {
// router: {
// navigate: jest.fn(),
// },
// store: {
// getters: {
// vehicle: {},
// },
// },
// },
// }) {
// //Mock api responses
// baseMixin.methods.dispatchStoreAction = jest.fn();
// const apiResponses = {
// cmsContent: {
// FunnelSubHeaderWidget: pageHeaderWidgetHeaderText,
// VehicleBannerWidget: {
// GenericVehicleImage:
// "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/blurred-image.jpg?sfvrsn=a6ce3034_3",
// },
// FunnelHeaderWidget: {
// LogoImage:
// "https://digitalconsumercms-dev.safelite.com/images/default-source/default-album/safelite-logo.svg?sfvrsn=45e7ed06_3",
// },
// StreetAddressQuestionWidget: {
// QuestionText:
// "test"
// },
// CityQuestionWidget: {
// QuestionText:
// "test"
// },
// StateQuestionWidget: {
// QuestionText:
// "test"
// },
// ZipQuestionWidget: {
// QuestionText:
// "test"
// },
// FirstNameQuestionWidget: {
// QuestionText:
// "test"
// },
// LastNameQuestionWidget: {
// QuestionText:
// "test"
// },
// EmailAddressQuestionWidget: {
// QuestionText:
// "test"
// },
// AlertVerificationWarningWidget: {
// HeaderText:
// "test",
// BodyText:
// "test",
// },
// AlertNoMatchWarningWidget: {
// HeaderText:
// "test",
// BodyText:
// "test",
// },
// },
// };
// const apiPromise = Promise.resolve(apiResponses);
// settleAllPromises.mockImplementation(() => apiPromise);
// fetchCmsContentForPage.mockImplementation(() => Promise.resolve());
// //Mock damage initialize methods
// funnelHeader.methods = {
// initializeComponent: jest.fn(),
// };
// vehicleBanner.methods = {
// initializeComponent: jest.fn(),
// };
// funnelSubHeader.methods = {
// initializeComponent: jest.fn(),
// };
// funnelFooter.methods = {
// initializeComponent: jest.fn(),
// };
// customerQuestions.methods = {
// initializeComponent: jest.fn(),
// };
// addressQuestions.methods = {
// initializeComponent: jest.fn(),
// setupAddressLookup: jest.fn(),
// };
// const mountOptions = getMountOptions(mountOptionsMockData);
// mountOptions['attachTo'] = document.body; // append wrapper to document.body to test DOM methods
// mountOptions.global.directives = {
// maska: maska
// };
// const wrapper = mount(addressLookup, mountOptions);
// const funnelHeaderWrapper = wrapper.findComponent({ name: "funnelHeader" });
// funnelHeaderWrapper.vm.initializeComponent =
// funnelHeader.methods.initializeComponent;
// const vehicleBannerWrapper = wrapper.findComponent({ name: "vehicleBanner" });
// vehicleBannerWrapper.vm.initializeComponent =
// vehicleBanner.methods.initializeComponent;
// const funnelSubHeaderWrapper = wrapper.findComponent({ name: "funnelSubHeader" });
// funnelSubHeaderWrapper.vm.initializeComponent =
// funnelSubHeader.methods.initializeComponent;
// const funnelFooterWrapper = wrapper.findComponent({ name: "funnelFooter" });
// funnelFooterWrapper.vm.initializeComponent =
// funnelFooter.methods.initializeComponent;
// const customerQuestionsWrapper = wrapper.findComponent({ name: "customerQuestions" });
// customerQuestionsWrapper.vm.initializeComponent =
// customerQuestions.methods.initializeComponent;
// const addressQuestionsWrapper = wrapper.findComponent({ name: "addressQuestions" });
// addressQuestionsWrapper.vm.initializeComponent =
// addressQuestions.methods.initializeComponent;
// addressQuestionsWrapper.vm.setupAddressLookup =
// addressQuestions.methods.setupAddressLookup;
// return { wrapper, apiPromise };
// }

View file

@ -151,15 +151,15 @@ export default {
);
},
attachCustomEvents() {
this.prependActionToMethod(this, this.forwardButtonAction, () => {
this.pushEventToGA(
this.$route.query[this.queryStrings.FMG_PAGE],
this.GaActions.SUBMITTED,
this.GaLabels.ADDRESS_LOOKUP,
true
);
});
},
this.prependActionToMethod(this, this.forwardButtonAction, () => {
this.pushEventToGA(
this.$route.query[this.queryStrings.FMG_PAGE],
this.GaActions.SUBMITTED,
this.GaLabels.ADDRESS_LOOKUP,
true
);
});
},
getRegistrationAddressFromStore() {
return store.getters.vehicle.registration.address;
},
@ -207,7 +207,7 @@ export default {
this.$refs.funnelFooter.removeLoader();
return;
}
// if the neither the registration zip code or service zip code are not serviceable
this.isZipServicable = serviceZipValidationResponse.data.isServiceable;
if (!this.isZipServicable) {
@ -368,22 +368,22 @@ export default {
}
},
mounted() {
this.attachCustomEvents();
},
this.attachCustomEvents();
},
computed: {
AlertNonServiceableZipHeader(){
AlertNonServiceableZipHeader() {
const zipCode = this.serviceZipCode ? this.serviceZipCode : this.customerQuestions.addressQuestions.zipCode;
const text = this.getCmsContent("AlertNonServiceableZipWidget", "HeadlineText").replaceAll("{custom:serviceZip}", zipCode);
return text;
},
AlertNonServiceableZipBody(){
AlertNonServiceableZipBody() {
return this.getCmsContent("AlertNonServiceableZipWidget", "BodyText");
},
AlertMatchedDifferentVehicleHeader(){
AlertMatchedDifferentVehicleHeader() {
const text = this.getCmsContent("AlertMatchedDifferentVehicleWidget", "HeadlineText").replaceAll("{custom:glassText}", getDamageString());
return text;
},
AlertMatchedDifferentVehicleBody(){
AlertMatchedDifferentVehicleBody() {
const vinYmmFound = `${this.customAlertData?.vehicleInfo?.year} ${this.customAlertData?.vehicleInfo?.make} ${this.customAlertData?.vehicleInfo?.model}`;
const vinYmmExpected = `${store.getters.vehicle.year} ${store.getters.vehicle.make} ${store.getters.vehicle.model}`;

View file

@ -0,0 +1,630 @@
// 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 { storeActions } from "@/constants/store-actions";
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 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
});
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: "Somewhere"
})
await wrapper.vm.$nextTick();
// Assert
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({});
wrapper.setData({
displayNoMatchWarning: true
});
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"
})
await wrapper.vm.$nextTick();
// Assert
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({});
wrapper.setData({
displayNoMatchWarning: true
});
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"
})
await wrapper.vm.$nextTick();
// Assert
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(),
navigateAfterSave: 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

@ -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;
@ -215,12 +214,12 @@ export default ({
// Standard place_changed event handling
const autocompleteListener = window.google.maps.event.addListener(autocomplete, 'place_changed', fillInAddress);
// 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.addEventListener("focus", () => {
addressField1.setAttribute("autocomplete", "do-not-autofill");
// 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.addEventListener("focus", () => {
addressField1.setAttribute("autocomplete", "do-not-autofill");
// Make place results box stick to the input on scroll
const streetAddressField = document.getElementById("streetAddressField");
@ -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 w
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

@ -0,0 +1,36 @@
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

@ -1,5 +1,5 @@
<template>
<addressQuestions ref="addressQuestions" v-model="customerModel.addressQuestions" :alertNotifications="alertNotifications" />
<addressQuestions ref="addressQuestions" v-model="customerModel.addressQuestions" />
<div class="row mb-4">
<div class="col">
<textboxQuestion

View file

@ -331,6 +331,11 @@ function setupMocks({
// lookupVin: jest.fn(() => Promise.resolve(lookupVinResponse)),
},
computed: {
dynamicStrings() {
return {ROUTER_LINK: "routerLink:"}
}
}
}
// mountOptions.propsData = {
// modelValue: modelValueProp,

View file

@ -28,8 +28,8 @@
/>
<div class="alert-provide-vin my-3" v-if="splitAlertProvideVinBodyForLink.length">
<span v-for="copy in splitAlertProvideVinBodyForLink" :key="copy">
<span v-if="copy.includes('routerLink:')" class="text-body">
<router-link :to="{query: {fmgPage: `${copy.split(':')[1].split(',')[0]}`}, name: 'root'}">{{ copy.split(':')[1].split(',')[1] }}</router-link>
<span v-if="doesCopyContainRouterLink(copy)" class="text-body">
<router-link :to="{query: {fmgPage: `${getRouterLinkRouteFromCopy(copy)}`}, name: 'root'}">{{ getRouterLinkDisplayTextFromCopy(copy) }}</router-link>
</span>
<span v-else class="m-0 text-body" v-html="copy"></span>
</span>
@ -68,6 +68,10 @@ import { required } from "@/helpers/validation-rules";
import { Form, defineRule } from "vee-validate";
import { navigateAfterSaveToHeritageFunnel } from "@/helpers/heritage-integration/navigation-helper";
import { isGlassAvailableForCarId } from "@/helpers/damage-helper";
import { doesCopyContainRouterLink,
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy, } from "@/helpers/cms-content-helper"
// DEFINE VALIDATION RULES
defineRule("vehicle-required", required(errorMessages.VEHICLE_REQUIRED));
@ -116,7 +120,7 @@ export default {
},
splitAlertProvideVinBodyForLink() {
// Splits content when brackets are found in text so that text can be looped through and router-link can be injected when needed
return this.AlertProvideVinBody.split(/{(.*?)}/g);
return this.splitCopyOnCMSPlaceHolder(this.AlertProvideVinBody);
},
VehiclesForQuestions() {
const vehiclesData = this.VehiclesFromApi;
@ -145,6 +149,10 @@ export default {
},
},
methods: {
doesCopyContainRouterLink,
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy,
arePagePrerequisitesValid() {
if (
store.getters.order.vehicle.carId

View file

@ -1089,7 +1089,7 @@
import textboxQuestion from "@/common-components/textbox-question/textbox-question";
import dropdownQuestion from "@/common-components/dropdown-question/dropdown-question";
import vinInformation from "@/layouts/vin-lookup/vin-information/vin-information";
import menuModal from "@/common-components/menu-modal/menu-modal";
import menuModal from "@/common-components/funnel-header/menu-modal/menu-modal";
export default {
name: "App",
components: {

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

@ -1 +1,204 @@
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 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();
});
});
function setupMocks({ customMountOptions }) {
const mountOptions = getMountOptions({});
const finalMountOptions = Object.assign(mountOptions, customMountOptions);
// Modify/augment default mount options
finalMountOptions.global.mocks["$store"] = store;
finalMountOptions.global.mixins = [mockMixin];
finalMountOptions['attachTo'] = document.body; // append wrapper to document.body to test DOM methods
const wrapper = shallowMount(vinLookup, finalMountOptions);
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

@ -292,8 +292,11 @@ 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();

View file

@ -10,10 +10,6 @@ import baseMixin from "@/mixins/base-mixin";
export default {
methods: {
logPageView(pageEvent) {
if (getSessionKeyValue() === 0 || getSessionIdValue() === '00000000-0000-0000-0000-000000000000') {
this.initSession();
}
const currentPageName = getPageNameByQueryString();
var payload = {
userId: getDeviceIdValue(),
@ -29,10 +25,6 @@ export default {
},
logCustomEvent(category, action, label, value) {
if (getSessionKeyValue() === 0 || getSessionIdValue() === '00000000-0000-0000-0000-000000000000') {
this.initSession();
}
const currentPageName = getPageNameByQueryString();
var payload = {
@ -50,28 +42,6 @@ export default {
baseMixin.methods.dispatchStoreAction(storeActions.LOG_CUSTOM_EVENT, payload, false);
},
async initSession() {
const sid = getSessionIdValue();
const skey = getSessionKeyValue();
var payload = {
userId: getDeviceIdValue(),
sessionId: sid,
userAgent: navigator.userAgent,
referrer: document.referrer,
};
const response = await baseMixin.methods.dispatchStoreAction(storeActions.INITIALIZE_SESSION, payload, false);
if (response.data) {
if (response.data.sessionKey && skey === 0) {
setCookieProperties({ [cookieNames.SESSION_KEY]: response.data.sessionKey});
}
if (response.data.sessionId && sid === '00000000-0000-0000-0000-000000000000') {
setCookieProperties({ [cookieNames.SESSION_ID]: response.data.sessionId});
}
}
},
pushEventToGA(category, action, label, pushToLogApp = false) {
const currentPageName = getPageNameByQueryString();
const eventToBePushed = {
@ -86,7 +56,7 @@ export default {
pushToDataLayerIfDefined(eventToBePushed);
if (pushToLogApp) {
this.logCustomEvent(category, action, label, undefined);
this.logCustomEvent(category, action, label, undefined);
}
},
@ -136,6 +106,32 @@ export default {
return baseMethod.apply(object, arguments);
};
},
async initSession() {
const sid = getSessionIdValue();
const skey = getSessionKeyValue();
var payload = {
userId: getDeviceIdValue(),
sessionId: sid,
userAgent: navigator.userAgent,
referrer: document.referrer,
};
const response = await baseMixin.methods.dispatchStoreAction(storeActions.INITIALIZE_SESSION, payload, false);
if (response.data) {
if (response.data.sessionKey && skey === 0) {
setCookieProperties({ [cookieNames.SESSION_KEY]: response.data.sessionKey});
}
if (response.data.sessionId && sid === '00000000-0000-0000-0000-000000000000') {
setCookieProperties({ [cookieNames.SESSION_ID]: response.data.sessionId});
}
}
},
noSession() {
return getSessionKeyValue() === 0 || getSessionIdValue() === '00000000-0000-0000-0000-000000000000';
}
},
computed: {
analyticsPageEvents() {

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

@ -5,6 +5,7 @@ import { navigationScenarios } from "@/router/router-constants/navigation-scenar
import { vehicleCategories } from "@/constants/vehicle-categories.js";
import { routerParams } from "@/router/router-constants/router-params";
import { queryStrings } from "@/constants/query-strings";
import { dynamicStrings } from "@/constants/dynamic-strings";
export default {
data() {
@ -65,6 +66,9 @@ export default {
},
queryStrings(){
return queryStrings;
},
dynamicStrings(){
return dynamicStrings;
}
},
};

View file

@ -40,8 +40,13 @@ const routes = [
async beforeEnter(to, from, next) {
// If we have no query string, or we don't have the FmgPage query string.
try {
if (analyticsMixin.methods.noSession()) {
await analyticsMixin.methods.initSession();
}
// If the saved session has timed out, clear the session, execute 404 logic.
if (getFunnelCookie() !== null && !isSavedSessionStillActive()) {
await baseMixin.methods.dispatchStoreAction(storeActions.RESET_STATE);
await GoToFunnelStartOn404(next);
}

View file

@ -53,8 +53,7 @@ body {
left: 50%;
transform: translateX(-50%);
max-width: 576px;
height: calc(100% - 152px);
top: 72px;
height: calc(100% - 72px);
}
}

View file

@ -1,23 +1,18 @@
import { shallowMount } from "@vue/test-utils";
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import alert from "./alert";
describe("alert.vue", () => {
it("Should add class 'alert-dismissible' if isDismissible is true", async () => {
// Arrange
const wrapper = shallowMount(alert, {
const wrapper = shallowMount(alert, setupMocks({
propsData: {
isDismissible: true
isDismissible: true,
manualHeadline: 'testHeader',
manualCopy: 'testCopy'
},
computed: {
splitAlertCopyForLink: {
get() {
return "TEST";
},
}
},
mixins: [mockMixin]
});
}));
const wrapperDiv = wrapper.find('div');
@ -27,19 +22,13 @@ describe("alert.vue", () => {
it("Should add specified alert class", async () => {
// Arrange
const wrapper = shallowMount(alert, {
const wrapper = shallowMount(alert, setupMocks({
propsData: {
alertClass: 'warning'
alertClass: 'warning',
manualHeadline: 'testHeader',
manualCopy: 'testCopy'
},
computed: {
splitAlertCopyForLink: {
get() {
return "TEST";
},
}
},
mixins: [mockMixin]
});
}));
const wrapperDiv = wrapper.find('div');
@ -49,24 +38,171 @@ describe("alert.vue", () => {
it("Should update alert Headline to manualHeadline datam entered and alert copy to manualCopy datam entered when no cmsWidgetName entered", async () => {
// Arrange
const wrapper = shallowMount(alert, {
propsData: {
manualHeadline: 'testHeader',
manualCopy: 'testCopy'
},
mixins: [mockMixin]
});
const wrapper = shallowMount(alert, setupMocks({}));
// Assert
expect(wrapper.vm.alertHeadline).toBe("testHeader");
expect(wrapper.vm.alertCopy).toBe("testCopy");
});
it("Should container a <router-link> tag if the manualCopy contains a {routerLink: testName, testLink} placeholder", () =>{
// Arrange & Act
const wrapper = shallowMount(alert, setupMocks({
propsData: {
manualHeadline: 'testHeader',
manualCopy: 'testCopy with a {routerLink: testName, testLink} inside of it'
},
stubs: ['router-link'],
}));
// Assert
expect(wrapper.find('router-link').exists()).toBe(true);
});
it("Should contain 'n+1' <p> tags if the body copy has 'n' <p> tags", () =>{
// Arrange & Act
const wrapper = shallowMount(alert, setupMocks({
propsData: {
manualHeadline: 'testHeader',
manualCopy: '<p>testCopy with a {routerLink: testName, testLink} inside of it</p><p>and two paragraphs</p>'
},
stubs: ['router-link'],
}));
// Assert
expect(wrapper.findAll('p').length === 3).toBe(true);
});
it("Should call scrollIntoView() when the clientBoundingRect is not entirely in the viewport (out of view top)", () => {
// Arrange
var viewPortHeight = 200;
setUpViewPort(viewPortHeight);
Element.prototype.getBoundingClientRect = jest.fn(()=> {
return {top: -100, bottom: 200}
});
var mockScrollIntoView = jest.fn();
Element.prototype.scrollIntoView = mockScrollIntoView;
// Act
const wrapper = shallowMount(alert, setupMocks({}));
// Assert
// This is an implementation detail - we just need to test that the final step of snapping
// the window to the alert is working. If using a different function to accomplish that
// just swap this out with the new function
expect(mockScrollIntoView).toHaveBeenCalled();
});
it("Should call scrollIntoView() when the clientBoundingRect is not entirely in the viewport (bottom is hidden behind footer)", () => {
// Arrange
var viewPortHeight = 240;
setUpViewPort(viewPortHeight);
Element.prototype.getBoundingClientRect = jest.fn(()=> {
return {top: 100, bottom: 200}
});
var mockScrollIntoView = jest.fn();
Element.prototype.scrollIntoView = mockScrollIntoView;
// Act
const wrapper = shallowMount(alert, setupMocks({}));
// Assert
// This is an implementation detail - we just need to test that the final step of snapping
// the window to the alert is working. If using a different function to accomplish that
// just swap this out with the new function
expect(mockScrollIntoView).toHaveBeenCalled();
});
it("Should not call scrollIntoView() when the clientBoundingRect is not entirely in the viewport but 'shouldScrollToOnMount' is false", () => {
// Arrange
var viewPortHeight = 200;
setUpViewPort(viewPortHeight);
Element.prototype.getBoundingClientRect = jest.fn(()=> {
return {top: -100, bottom: 200}
});
var mockScrollIntoView = jest.fn();
Element.prototype.scrollIntoView = mockScrollIntoView;
// Act
const wrapper = shallowMount(alert, setupMocks({
propsData: {
shouldScrollToOnMount: false,
manualHeadline: 'testHeader',
manualCopy: 'testCopy'
},
}));
// Assert
// This is an implementation detail - we just need to test that the final step of snapping
// the window to the alert is working. If using a different function to accomplish that
// just swap this out with the new function
expect(mockScrollIntoView).not.toHaveBeenCalled();
});
it("Should not call scrollIntoView() when the clientBoundingRect is entirely in the viewport", () => {
// Arrange
var viewPortHeight = 500;
setUpViewPort(viewPortHeight);
Element.prototype.getBoundingClientRect = jest.fn(()=> {
return {top: 100, bottom: 200}
});
var mockScrollIntoView = jest.fn();
Element.prototype.scrollIntoView = mockScrollIntoView;
// Act
const wrapper = shallowMount(alert, setupMocks({}));
// Assert
// This is an implementation detail - we just need to test that the final step of snapping
// the window to the alert is working. If using a different function to accomplish that
// just swap this out with the new function
expect(mockScrollIntoView).not.toHaveBeenCalled();
});
});
const mockMixin = {
methods: {
getCmsContent: jest.fn(),
getFooterInfoBoxHeight: jest.fn(()=> 80),
getFooterInfoBoxHeight: jest.fn(()=> 50),
},
computed: {
dynamicStrings: jest.fn(()=> {
return {ROUTER_LINK: 'routerLink:'}
})
}
}
function setUpViewPort(height) {
Object.defineProperty(global.window, 'innerHeight', {
writable: true,
configurable: true,
value: height,
});
Object.defineProperty(window.document.documentElement, 'clientHeight', {
writable: true,
configurable: true,
value: height
});
}
function setupMocks(mountOptionsMockData = {}) {
const defaultMountOptions = {
propsData: {
manualHeadline: 'testHeader',
manualCopy: 'testCopy'
},
mixins: [mockMixin]
};
const baseMountOptions = getMountOptions(Object.assign(defaultMountOptions, mountOptionsMockData));
const allMountOptions = Object.assign(defaultMountOptions, baseMountOptions);
return allMountOptions;
}

View file

@ -8,7 +8,7 @@
<template v-for="paragraph in splitAlertCopyForParagraphTag" :key="paragraph">
<p class="m-0 text-body small" v-if="!doesCopyContainRouterLink(paragraph)" v-html="paragraph"></p>
<p class="m-0 text-body small" v-else>
<template v-for="copy in splitCopyForRouterLink(paragraph)" :key="copy">
<template v-for="copy in splitCopyOnCMSPlaceHolder(paragraph)" :key="copy">
<span v-if="!doesCopyContainRouterLink(copy)" v-html="copy"></span>
<span v-else>
<router-link :to="{query: {fmgPage: `${getRouterLinkRouteFromCopy(copy)}`}, name: 'root'}">{{ getRouterLinkDisplayTextFromCopy(copy) }}</router-link>
@ -36,6 +36,11 @@
</template>
<script>
import { doesCopyContainRouterLink,
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy,
splitCMSCopyOnParagraphTag } from "@/helpers/cms-content-helper"
export default {
name: "alert",
@ -65,36 +70,19 @@ export default {
return this.cmsWidgetName ? this.getCmsContent(this.cmsWidgetName, 'BodyText') : this.manualCopy;
},
splitAlertCopyForParagraphTag(){
// splits the alertCopy on <p ... > (with or without attributes) and </p>
// filter removes empty strings that are a result of string.split with regex
return this.alertCopy.split(/(?:<p(?:.*?)>)|(?:<\/p>)/g).filter(paragraph => paragraph !== "");
return splitCMSCopyOnParagraphTag(this.alertCopy);
},
},
methods: {
doesCopyContainRouterLink(copy) {
return copy.includes('routerLink:');
},
splitCopyForRouterLink(copy){
// splits copy on { ... } such as {routerlink: ...}
return copy.split(/{(.*?)}/g);
},
getRouterLinkRouteFromCopy(copy){
// sample input: {routerLink:estimate,provide your VIN}
// first split would return 'estimate,provide your VIN'
// second split would return 'estimate'
return copy.split(':')[1].split(',')[0];
},
getRouterLinkDisplayTextFromCopy(copy){
// sample input: {routerLink:estimate,provide your VIN}
// first split would return 'estimate,provide your VIN'
// second split would return 'provide your VIN'
return copy.split(':')[1].split(',')[1];
},
doesCopyContainRouterLink,
splitCopyOnCMSPlaceHolder,
getRouterLinkRouteFromCopy,
getRouterLinkDisplayTextFromCopy,
ensureAlertIsInViewPort() {
if (this.shouldScrollToOnMount && this.$el.style.display != 'none') {
var footerHeight = this.getFooterInfoBoxHeight();
if (!this.isAlertInViewport(footerHeight)) {
this.scrollContainerToAlert(footerHeight);
this.$el.scrollIntoView(true); // 'true' attempts to scroll element to top of viewport
}
}
},
@ -106,14 +94,6 @@ export default {
rect.bottom <= (window.innerHeight - footerHeight || document.documentElement.clientHeight - footerHeight)
);
},
scrollContainerToAlert(footerHeight) {
// alert position on page + height of alert + footer height
var scrollToHeight = this.$el.scrollHeight + this.$el.offsetHeight + footerHeight;
// find the div wrapped by the form element - this is the scrollable container
// should be a more future-proof selector in case of CSS class changes
var pageContainerScrollable = document.querySelector('form > div');
pageContainerScrollable.scrollTo(0, scrollToHeight);
},
},
mounted() {
this.ensureAlertIsInViewPort();