Merge pull request #530 from Safelite/feature/CSR-563
CSR-563 | Refactor Alerts and implement multiple unit tests
This commit is contained in:
commit
9f6f199c7a
7 changed files with 235 additions and 69 deletions
|
|
@ -1,7 +1,7 @@
|
|||
const dynamicStrings = {
|
||||
GLOBAL_STATE: "globalState",
|
||||
CUSTOM: "custom",
|
||||
ROUTER_LINK: "routerLink"
|
||||
ROUTER_LINK: "routerLink:"
|
||||
};
|
||||
|
||||
export { dynamicStrings };
|
||||
|
|
@ -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 !== "");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -331,6 +331,11 @@ function setupMocks({
|
|||
// lookupVin: jest.fn(() => Promise.resolve(lookupVinResponse)),
|
||||
|
||||
},
|
||||
computed: {
|
||||
dynamicStrings() {
|
||||
return {ROUTER_LINK: "routerLink:"}
|
||||
}
|
||||
}
|
||||
}
|
||||
// mountOptions.propsData = {
|
||||
// modelValue: modelValueProp,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue