diff --git a/src/constants/dynamic-strings.js b/src/constants/dynamic-strings.js
index 1c3b8ade7..c526e45a9 100644
--- a/src/constants/dynamic-strings.js
+++ b/src/constants/dynamic-strings.js
@@ -1,7 +1,7 @@
const dynamicStrings = {
GLOBAL_STATE: "globalState",
CUSTOM: "custom",
- ROUTER_LINK: "routerLink"
+ ROUTER_LINK: "routerLink:"
};
export { dynamicStrings };
\ No newline at end of file
diff --git a/src/helpers/cms-content-helper.js b/src/helpers/cms-content-helper.js
index 642f0d014..73912835a 100644
--- a/src/helpers/cms-content-helper.js
+++ b/src/helpers/cms-content-helper.js
@@ -120,4 +120,37 @@ function processWidgetItemForReplacement(widgetModel, key) {
// If we have something else like a number, boolean, etc. just return it
return widgetModel[key];
-}
\ No newline at end of file
+}
+
+
+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
+//
...
+// 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>)/g).filter(paragraph => paragraph !== "");
+}
diff --git a/src/layouts/address-vehicles/address-vehicles.vue b/src/layouts/address-vehicles/address-vehicles.vue
index 34937bfb7..22a237e75 100644
--- a/src/layouts/address-vehicles/address-vehicles.vue
+++ b/src/layouts/address-vehicles/address-vehicles.vue
@@ -28,8 +28,8 @@
/>
-
- {{ copy.split(':')[1].split(',')[1] }}
+
+ {{ getRouterLinkDisplayTextFromCopy(copy) }}
@@ -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
diff --git a/src/mixins/base-mixin.js b/src/mixins/base-mixin.js
index d0474d67b..c3840e780 100644
--- a/src/mixins/base-mixin.js
+++ b/src/mixins/base-mixin.js
@@ -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;
}
},
};
diff --git a/src/ux-components/alert/alert.spec.js b/src/ux-components/alert/alert.spec.js
index a0e65b12f..749a664b7 100644
--- a/src/ux-components/alert/alert.spec.js
+++ b/src/ux-components/alert/alert.spec.js
@@ -1,23 +1,18 @@
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,34 +38,106 @@ 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 run scrollContainerToAlert function when the clientBoundingRect is not entirely in the viewport", () => {
+ it("Should call scrollIntoView() when the clientBoundingRect is not entirely in the viewport (out of view top)", () => {
// Arrange
- const wrapper = shallowMount(alert, {
- computed: {
- splitAlertCopyForLink: {
- get() {
- return "TEST";
- },
- }
- },
- mixins: [mockMixin]
- });
+ 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();
+
});
});
@@ -84,6 +145,38 @@ describe("alert.vue", () => {
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;
}
\ No newline at end of file
diff --git a/src/ux-components/alert/alert.vue b/src/ux-components/alert/alert.vue
index 1d983bd42..d356d0569 100644
--- a/src/ux-components/alert/alert.vue
+++ b/src/ux-components/alert/alert.vue
@@ -8,7 +8,7 @@
-
+
{{ getRouterLinkDisplayTextFromCopy(copy) }}
@@ -36,6 +36,11 @@