From bca83d47836d73f4cad645182410b7cb0138b8ba Mon Sep 17 00:00:00 2001 From: Jason Wheeler Date: Mon, 7 Nov 2022 14:15:17 -0500 Subject: [PATCH 1/4] Added alert component --- .../site-header/site-header.vue | 32 ++- src/helpers/cms-content-helper.js | 34 ++- src/ux-components/alert/alert.spec.js | 222 ++++++++++++++++++ src/ux-components/alert/alert.vue | 189 +++++++++++++++ 4 files changed, 472 insertions(+), 5 deletions(-) create mode 100644 src/ux-components/alert/alert.spec.js create mode 100644 src/ux-components/alert/alert.vue diff --git a/src/common-components/site-header/site-header.vue b/src/common-components/site-header/site-header.vue index 76de47be..82e6c2ae 100644 --- a/src/common-components/site-header/site-header.vue +++ b/src/common-components/site-header/site-header.vue @@ -1,14 +1,37 @@ diff --git a/src/helpers/cms-content-helper.js b/src/helpers/cms-content-helper.js index 356ffb76..c4767e61 100644 --- a/src/helpers/cms-content-helper.js +++ b/src/helpers/cms-content-helper.js @@ -118,4 +118,36 @@ function mapStringToState(str) { } return stringBuilder.trimStart(); - } \ 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/ux-components/alert/alert.spec.js b/src/ux-components/alert/alert.spec.js new file mode 100644 index 00000000..343323b9 --- /dev/null +++ b/src/ux-components/alert/alert.spec.js @@ -0,0 +1,222 @@ +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, + setupMocks({ + propsData: { + isDismissible: true, + manualHeadline: "testHeader", + manualCopy: "testCopy", + }, + }) + ); + + const wrapperDiv = wrapper.find("div"); + + // Assert + expect(wrapperDiv.classes()).toContain("alert-dismissible"); + }); + + it("Should add specified alert class", async () => { + // Arrange + const wrapper = shallowMount( + alert, + setupMocks({ + propsData: { + alertClass: "warning", + manualHeadline: "testHeader", + manualCopy: "testCopy", + }, + }) + ); + + const wrapperDiv = wrapper.find("div"); + + // Assert + expect(wrapperDiv.classes()).toContain("warning"); + }); + + 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, setupMocks({})); + // Assert + expect(wrapper.vm.alertHeadline).toBe("testHeader"); + expect(wrapper.vm.alertCopy).toBe("testCopy"); + }); + + it("Should container a 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'

tags if the body copy has 'n'

tags", () => { + // Arrange & Act + const wrapper = shallowMount( + alert, + setupMocks({ + propsData: { + manualHeadline: "testHeader", + manualCopy: + "

testCopy with a {routerLink: testName, testLink} inside of it

and two paragraphs

", + }, + 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(() => 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; +} diff --git a/src/ux-components/alert/alert.vue b/src/ux-components/alert/alert.vue new file mode 100644 index 00000000..d0fe33ac --- /dev/null +++ b/src/ux-components/alert/alert.vue @@ -0,0 +1,189 @@ + + + + + From 652eed2585f56ee8a72160fbc73909f360f5299e Mon Sep 17 00:00:00 2001 From: Jason Wheeler Date: Tue, 8 Nov 2022 16:11:40 -0500 Subject: [PATCH 2/4] Added EventBus to create new alert events --- .../site-header/site-header.vue | 14 ++++ src/constants/events.js | 17 ++++ src/helpers/event-bus/event-bus.js | 36 +++++++++ src/helpers/event-bus/event-bus.spec.js | 78 +++++++++++++++++++ src/store/index.js | 30 ++++++- src/store/store.spec.js | 73 +++++++++++++++-- 6 files changed, 241 insertions(+), 7 deletions(-) create mode 100644 src/constants/events.js create mode 100644 src/helpers/event-bus/event-bus.js create mode 100644 src/helpers/event-bus/event-bus.spec.js diff --git a/src/common-components/site-header/site-header.vue b/src/common-components/site-header/site-header.vue index 82e6c2ae..b9499a9f 100644 --- a/src/common-components/site-header/site-header.vue +++ b/src/common-components/site-header/site-header.vue @@ -18,6 +18,8 @@ \ No newline at end of file diff --git a/src/router/router-constants/issPage-values.js b/src/router/router-constants/issPage-values.js index 07ea81bd..c31fa083 100644 --- a/src/router/router-constants/issPage-values.js +++ b/src/router/router-constants/issPage-values.js @@ -1,5 +1,4 @@ export const issPageValues = { - ERROR_404: "error-404", WELCOME_PAGE: "welcome-page", VEHICLE_MAKE: "vehicle-make", VEHICLE_YEAR: "vehicle-year", diff --git a/src/store/store.spec.js b/src/store/store.spec.js index 49444767..9f5db937 100644 --- a/src/store/store.spec.js +++ b/src/store/store.spec.js @@ -3,6 +3,7 @@ import { createApp } from 'vue'; import { createPinia } from "pinia"; import App from '@/App.vue'; + describe("Store", () => { let store; @@ -36,7 +37,7 @@ describe("Store", () => { }, }; - store.addEventToBus(store, event); + store.addEventToBus(event); expect(store.applicationUser.eventBus[0]).toEqual(event); }); @@ -53,11 +54,11 @@ describe("Store", () => { }, }; - store.addEventToBus(store, event); + store.addEventToBus(event); expect(store.applicationUser.eventBus.length).toBe(1); - store.removeEventFromBus(store, { category: event.category, subCategory: event.subCategory }) + store.removeEventFromBus({ category: event.category, subCategory: event.subCategory }) expect(store.applicationUser.eventBus.length).toBe(0); }); @@ -74,7 +75,7 @@ describe("Store", () => { }, }; - store.addEventToBus(store, event); + store.addEventToBus(event); const actual = store.eventBusItem(event.category, event.subCategory)