Merge remote-tracking branch 'origin/develop' into feature/unit-tests-2022.06.09-KO

This commit is contained in:
Katie 2022-06-08 08:06:41 -04:00
commit 306499cf95
17 changed files with 282 additions and 116 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

@ -24,7 +24,6 @@ module.exports = {
"!src/layouts/address-lookup/customer-questions/customer-questions.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

@ -14,9 +14,9 @@
<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
@ -45,6 +45,9 @@ export default {
myModalEl.addEventListener('show.bs.modal', function (event) {
self.currentFooterAndHeaderHeight = self.getFooterInfoBoxHeight() + 72;
self.isActive = true;
document.querySelector('.page-container-grouped-styles').scrollTo({
top: 0, behavior: 'smooth'
});
})
},
components: {
@ -71,6 +74,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

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

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

@ -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();