merged from release/2022.06.09

This commit is contained in:
Leah Schumann 2022-06-07 09:22:01 -04:00
commit be09c6342d
22 changed files with 733 additions and 293 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,10 +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
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

@ -25,7 +25,6 @@ module.exports = {
"!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",
// END
], // ! means exclude from coverage.
testMatch: ["**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"],

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,6 +73,7 @@ export default {
flex-direction: column;
justify-content: center;
align-items: center;
padding: 0;//Required to prevent 'squish' on iPhone
z-index: 1056;
.bar1,
.bar2,

View file

@ -3,7 +3,7 @@
<div class="row g-2 h-100 d-flex align-items-center">
<div class="container-fluid overflow-hidden">
<div class="row h-100">
<div class="col text-center tagbg mb-2">
<div class="col text-center tagbg mb-4">
<div class="spinner-border text-danger" role="status">
<span class="visually-hidden">Loading...</span>
</div>
@ -12,17 +12,7 @@
<div class="row">
<div class="col">
<div class="text-container slide">
<p>Finishing up
<span class="dot-1">.</span>
<span class="dot-2">.</span>
<span class="dot-3">.</span>
</p>
<p>Nearly there
<span class="dot-1">.</span>
<span class="dot-2">.</span>
<span class="dot-3">.</span>
</p>
<p>Generating your quote
<p>Assessing your damage
<span class="dot-1">.</span>
<span class="dot-2">.</span>
<span class="dot-3">.</span>
@ -32,7 +22,17 @@
<span class="dot-2">.</span>
<span class="dot-3">.</span>
</p>
<p>Assessing your damage
<p>Generating your quote
<span class="dot-1">.</span>
<span class="dot-2">.</span>
<span class="dot-3">.</span>
</p>
<p>Nearly there
<span class="dot-1">.</span>
<span class="dot-2">.</span>
<span class="dot-3">.</span>
</p>
<p>Finishing up
<span class="dot-1">.</span>
<span class="dot-2">.</span>
<span class="dot-3">.</span>
@ -130,39 +130,39 @@ export default {
p {
margin: 0 175px;
transform: translateX(-2346px);
transform: translateX(180px);
}
@keyframes slide-in {
0% {
transform: translateX(-2346px)
transform: translateX(180px)
}
11% {
transform: translateX(-2010px)
transform: translateX(-80px)
}
22% {
transform: translateX(-2010px)
transform: translateX(-80px)
}
33% {
transform: translateX(-1490px)
transform: translateX(-600px)
}
44% {
transform: translateX(-1490px)
transform: translateX(-600px)
}
55% {
transform: translateX(-985px)
transform: translateX(-1110px)
}
66% {
transform: translateX(-985px)
transform: translateX(-1110px)
}
77% {
transform: translateX(-490px)
transform: translateX(-1600px)
}
88% {
transform: translateX(-490px)
transform: translateX(-1600px)
}
100% {
transform: translateX(-40px)
transform: translateX(-2050px)
}
}

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

@ -329,8 +329,8 @@ export default {
{
licenseLastName: lastName,
licenseStreetAddress: streetAddress,
licenseZip: zip,
licenseState: state
licenseZip: zip,
licenseState: state,
}, false
);
},

View file

@ -0,0 +1,63 @@
import { shallowMount } from "@vue/test-utils";
import addressVehiclesQuestion from "@/layouts/address-vehicles/address-vehicles-question/address-vehicles-question";
describe("addressVehiclesQuestion.vue", () => {
it("Should return content for differentVehicleAlertHeader", () => {
// Arrange
const wrapper = shallowMount(addressVehiclesQuestion, {
mixins: [mockMixin],
});
// Assert
expect(wrapper.vm.differentVehicleAlertHeader).toEqual('FoundWindshieldTestReturn');
});
it("Should return content for differentVehicleAlertBody", () => {
// Arrange
const wrapper = shallowMount(addressVehiclesQuestion, {
mixins: [mockMixin],
propsData: {
vehicles: ["1", "2"],
modelValue: ["1", "2"],
}
});
// Assert
expect(wrapper.vm.differentVehicleAlertBody).toEqual('FoundWindshieldTestReturn');
});
it("Should emit a modelValue change when setting selectedVehicleVin", async () => {
// Arrange
const wrapper = shallowMount(addressVehiclesQuestion, {
mixins: [mockMixin],
propsData: {
vehicles: ["1", "2"],
modelValue: ["1", "2"],
}
});
// Act
const localThis = { $emit: jest.fn() }
addressVehiclesQuestion.computed.selectedVehicleVin.set.call(localThis, 'newValue');
// Assert
expect(localThis.$emit).toBeCalledWith("update:modelValue", "newValue");
});
});
const mockMixin = {
methods: {
getCmsContent: jest.fn((contentName) => {
if (contentName === "FoundWindshield") {
return 'FoundWindshieldTestReturn';
}
return null;
}),
vehicles: jest.fn(() => {
return [{ vehicle: "test" }];
})
}
}

View file

@ -1,113 +0,0 @@
import { shallowMount } from "@vue/test-utils";
import addressVehiclesQuestion from "@/layouts/address-vehicles/address-vehicles-question/address-vehicles-question";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
// import store from "@/store";
// jest.mock("@/store", () => { return {}; }, { virtual: true });
describe("addressVehiclesQuestion.vue", () => {
it("Should include button-question component", () => {
// Arrange
const wrapper = shallowMount(addressVehiclesQuestion, {
propsData: {
// vehicles: [
// {
// "Name": "testname",
// "Text": "testtext",
// }
// ]
}
});
// console.log('wrapper.html: ', wrapper.html());
// Assert
const buttonQuestion = wrapper.find('button-question-stub');
expect(buttonQuestion).toBe;
});
it("on initialize should pass in questionText", () => {
// Arrange
const wrapper = shallowMount(addressVehiclesQuestion, {
propsData: {},
});
//Act
addressVehiclesQuestion.methods.initializeComponent.call(wrapper.vm, {QuestionText: 'Testing question text'});
// console.log('wrapper.html: ', wrapper.html());
// console.log('wrapper.vm.questionText: ', wrapper.vm.questionText);
// Assert
expect(wrapper.vm.questionText).toEqual('Testing question text');
});
// it("Alert should show if prop isCarIdDifferent is true", () => {
// // Arrange
// const wrapper = shallowMount(addressVehiclesQuestion, setupMountOptions({
// propsData: {
// isCarIdDifferent: true,
// }
// }));
// //Act
// const alert = wrapper.find('alert');
// console.log('wrapper.html: ', wrapper.html());
// console.log('wrapper.vm.questionText: ', wrapper.vm.questionText);
// // Assert
// // expect(wrapper.vm.questionText).toEqual('Testing question text');
// });
});
function setupMountOptions(mountOptionsMockData = {}) {
// //Mock store
// store.dispatch = jest.fn(() => {});
// store.getters = {};
// const mockMixin = {
// methods: {
// getCmsContent: jest.fn().mockImplementation(() => {
// return '';
// }),
// getDamageString: jest.fn().mockImplementation(() => {
// return '';
// })
// },
// store: {
// dispatch: store.dispatch,
// getters: store.getters,
// },
// }
const mockGetCmsContent = jest.fn();
mockGetCmsContent((cmsWidget, field) => {
return field
});
const defaultMountOptions = {
// route: { query: { fmgPage: 'page-name' } },
mixins: {
methods: {
getCmsContent: mockGetCmsContent,
},
},
global: {
mocks: {
store: {
dispatch: store.dispatch,
getters: store.getters,
},
},
},
};
const baseMountOptions = getMountOptions(Object.assign(defaultMountOptions, mountOptionsMockData));
const allMountOptions = Object.assign(defaultMountOptions, baseMountOptions);
console.log('what are allMountOptions??? ', allMountOptions);
return allMountOptions;
}

View file

@ -0,0 +1,354 @@
// Components
import addressVehicles from "@/layouts/address-vehicles/address-vehicles";
// Supporting Files
import { shallowMount } from "@vue/test-utils";
import { getMountOptions } from "@/helpers/unit-test-helper.js";
import store from "@/store";
import * as navigateToHeritage from "@/helpers/heritage-integration/navigation-helper";
// Mock our module for promises.
jest.mock("@/helpers/damage-helper", () => ({
isGlassAvailableForCarId: () => {
return false;
},
}));
describe("addressVehicles.vue", () => {
test("Should return true for valid page requisites if carId / zipCode / emailAddress / pageData exists", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.$router.navigate = jest.fn();
// Act
const result = wrapper.vm.arePagePrerequisitesValid();
//Assert
expect(result).toBe(true);
wrapper.unmount();
});
test("Should return false for valid page requisites if carId is missing", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.$router.navigate = jest.fn();
// Act
wrapper.vm.$store.getters.order.vehicle.carId = null;
const result = wrapper.vm.arePagePrerequisitesValid();
//Assert
expect(result).toBe(false);
wrapper.unmount();
});
// NOTE: this test is only here to meet code coverage; it does not test any logic in the original function
test("Should navigate to CLICKED_BACK if backButtonAction is run", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.$router.navigate = jest.fn();
// Act
wrapper.setData({
selectedVehicleVin: ['5NMS3CADXLH233004'],
});
wrapper.vm.backButtonAction();
//Assert
expect(wrapper.vm.$router.navigate).toBeCalled();
wrapper.unmount();
});
// NOTE: this test is only here to meet code coverage; it does not test any logic in the original function
test("Should run several related methods if forwardButtonAction is run", async () => {
// Arrange
const { wrapper } = setupMocks({});
const lookupVinResponse = {
data: {
carId: "456"
}
}
// the following has to be set BEFORE changing the data which is being watched, and requires updateButtonText to be mocked
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn();
wrapper.vm.lookupVin = jest.fn(() => Promise.resolve(lookupVinResponse));
wrapper.vm.$router.navigateAfterSave = jest.fn();
wrapper.vm.updateCustomerInfo = jest.fn().mockImplementation(()=> {});
wrapper.vm.navigateForward = jest.fn().mockImplementation(()=> {});
// Act
wrapper.setData({
selectedVehicleVin: ['5NMS3CADXLH233004'],
});
await wrapper.vm.forwardButtonAction();
wrapper.vm.$nextTick();
//Assert
expect(wrapper.vm.updateCustomerInfo).toBeCalled();
expect(wrapper.vm.navigateForward).toBeCalled();
wrapper.unmount();
});
test("Should return out of forwardButtonAction if lookupVin returns with an error", async () => {
// Arrange
const { wrapper } = setupMocks({});
const lookupVinResponse = {
error: "there is an error"
}
// the following has to be set BEFORE changing the data which is being watched, and requires updateButtonText to be mocked
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
wrapper.vm.$refs.funnelFooter.removeLoader = jest.fn();
wrapper.vm.lookupVin = jest.fn(() => Promise.reject(lookupVinResponse));
wrapper.vm.$router.navigateAfterSave = jest.fn();
wrapper.vm.updateCustomerInfo = jest.fn().mockImplementation(()=> {});
// Act
wrapper.setData({
selectedVehicleVin: ['5NMS3CADXLH233004'],
});
await wrapper.vm.forwardButtonAction();
wrapper.vm.$nextTick();
//Assert
expect(wrapper.vm.forwardButtonAction).toReturn;
wrapper.unmount();
});
test("Should send dispatch reset if carId is different and selected glass not available for vehicle on updateCustomerInfo", async () => {
// Arrange
const { wrapper } = setupMocks({});
const lookupVinResponse = {
data: {
carId: "456"
}
}
// the following has to be set BEFORE changing the data which is being watched, and requires updateButtonText to be mocked
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
wrapper.vm.lookupVin = jest.fn(() => Promise.resolve(lookupVinResponse));
wrapper.vm.$router.navigateAfterSave = jest.fn();
// Act
wrapper.setData({
selectedVehicleVin: ['5NMS3CADXLH233004'],
isSelectedGlassAvailableForVehicle: false,
isCarIdDifferent: true,
});
await wrapper.vm.updateCustomerInfo(wrapper.vm.selectedVehicle.vin, wrapper.vm.selectedVehicle.vehicle);
//Assert
expect(store.dispatch).toBeCalledWith("resetDamageAndDependencies");
wrapper.unmount();
});
// NOTE: this test is only here to meet code coverage; it does not test any logic in the original function
test("Should send dispatch store action if lookupVin is called", async () => {
// Arrange
const { wrapper } = setupMocks({});
// Act
await wrapper.vm.lookupVin('1234567890');
//Assert
expect(store.dispatch).toBeCalledWith("lookupVehicleByVin", {"vin": "1234567890"});
wrapper.unmount();
});
test("If selectedVehicleVin changes, then should update isCarIdDifferent", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
// Act
wrapper.setData({
selectedVehicleVin: ['5NMS3CADXLH233004'],
isCarIdDifferent: false,
});
await wrapper.vm.resetDependentState();
//Assert
expect(wrapper.vm.isCarIdDifferent).toBe(true);
wrapper.unmount();
});
test("If selectedVehicleVin changes, then text on funnel footer should be updated", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
// Act
wrapper.setData({
selectedVehicleVin: ['5NMS3CADXLH233004'],
isCarIdDifferent: false,
});
await wrapper.vm.resetDependentState();
//Assert
expect(wrapper.vm.$refs.funnelFooter.updateButtonText).toBeCalled();
wrapper.unmount();
});
test("Should navigate to CLICKED_FORWARD scenario if carId is different and selected glass not available for vehicle on navigateForward", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
wrapper.vm.$router.navigateAfterSave = jest.fn();
// Act
wrapper.setData({
selectedVehicleVin: ['5NMS3CADXLH233004'],
isSelectedGlassAvailableForVehicle: false,
isCarIdDifferent: true,
});
await wrapper.vm.navigateForward();
//Assert
expect(wrapper.vm.$router.navigateAfterSave).toBeCalledTimes(1);
wrapper.unmount();
});
test("Should navigate to navigateAfterSaveToHeritageFunnel if carId is not different on navigateForward", async () => {
// Arrange
const { wrapper } = setupMocks({});
wrapper.vm.$refs.funnelFooter.updateButtonText = jest.fn();
wrapper.vm.$refs.loadingModal.showModal = jest.fn();
navigateToHeritage.navigateAfterSaveToHeritageFunnel = jest.fn();
// Act
wrapper.setData({
selectedVehicleVin: ['5NMS3CADXLH233004'],
isSelectedGlassAvailableForVehicle: true,
isCarIdDifferent: false,
});
await wrapper.vm.navigateForward();
//Assert
expect(navigateToHeritage.navigateAfterSaveToHeritageFunnel).toBeCalledTimes(1);
wrapper.unmount();
});
});
function setupMocks({
// modelValueProp = "1900",
cmsQuestionText = "CMS text goes here",
// dataFromStoreApi = [],
}) {
//Mock store
store.dispatch = jest.fn(() => {});
store.getters = {
pageData: jest.fn((pageName) => {
// console.log('pageName: ', pageName) // address-vehicles
return [
{
vehicle: {
"carId": "CR00069309",
"category": "SUV",
"year": 2020,
"make": "Hyundai",
"model": "Santa Fe",
"style": "4 door utility",
"imageUrl": "https://dbhdyzvm8lm25.cloudfront.net/color_0320_032/MY2020/13769/13769_cc0320_032_WW8.jpg",
"imageVifNumber": "13769",
"imageVifColor": "white"
},
vin: "5NMS3CADXLH233004"
},
];
}),
order: {
vehicle: {
carId: "123",
},
serviceLocation: {
zipCode: "12345"
},
customer: {
emailAddress: "qw@er.ty"
}
},
damage: {
glassToReplace: "Windshield"
},
vehicle: {
carId: "456",
}
};
// baseMixin.methods.dispatchStoreAction = jest.fn();
const mountOptions = getMountOptions({
store: {
dispatch: store.dispatch,
getters: store.getters,
},
router: {
navigate: jest.fn(),
},
});
//Mock props
const mockMixin = {
methods: {
getCmsContent: jest.fn((contentName) => {
if (contentName === "FoundMultipleVehicles") {
return 'FoundMultipleVehiclesTestReturn';
}
if (contentName === "ProvideVinAlert") {
return 'ProvideVinAlertTestReturn';
}
return null;
}),
// dispatchStoreAction: jest.fn(() => {
// console.log("23424243")
// }),
// isGlassAvailableForCarId: () => {
// console.log("%%%%%%%%%%%%%%%")
// return Promise.resolve(true)
// }
// lookupVin: jest.fn(() => Promise.resolve(lookupVinResponse)),
},
computed: {
dynamicStrings() {
return {ROUTER_LINK: "routerLink:"}
}
}
}
// mountOptions.propsData = {
// modelValue: modelValueProp,
// };
mountOptions.mixins = [mockMixin];
const wrapper = shallowMount(addressVehicles, mountOptions);
//Mock CMS content
const cmsContent = {
QuestionText: cmsQuestionText,
};
return { wrapper };
}

View file

@ -1,26 +0,0 @@
import { shallowMount } from "@vue/test-utils";
import addressVehicles from "@/layouts/address-vehicles/address-vehicles";
describe("addressVehicles.vue", () => {
it("Should include address-vehicles-question component", () => {
// Arrange
const wrapper = shallowMount(addressVehicles, {
propsData: {
vehicles: [
{
"Name": "testname",
"Text": "testtext",
}
]
}
});
// console.log('wrapper.html: ', wrapper.html());
// Assert
const addressVehiclesQuestion = wrapper.find('address-vehicles-question-stub');
expect(addressVehiclesQuestion).toBe;
});
});

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
@ -162,8 +170,10 @@ export default {
async forwardButtonAction() {
const vinLookup = await this.lookupVin(this.selectedVehicle.vin).catch(() => {
this.$refs.funnelFooter.removeLoader();
return;
});
if (!vinLookup) {
return;
}
this.isSelectedGlassAvailableForVehicle = await isGlassAvailableForCarId(vinLookup.data.carId);
this.updateCustomerInfo(this.selectedVehicle.vin, this.selectedVehicle.vehicle);
this.navigateForward();
@ -210,7 +220,7 @@ export default {
},
watch: {
selectedVehicleVin(vehicleVin) {
selectedVehicleVin() {
// does this vehicle match the previously selected carId?
this.isCarIdDifferent = this.selectedVehicle.vehicle.carId !== store.getters.vehicle.carId;
this.$refs.funnelFooter.updateButtonText(`Continue with ${this.selectedVehicle.vehicle.year} ${this.selectedVehicle.vehicle.make} ${this.selectedVehicle.vehicle.model}`);

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

@ -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

@ -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,6 +40,10 @@ 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 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();