Merge pull request #39 from Safelite/feature/Digital/SSR-117
Feature/digital/ssr 117
This commit is contained in:
commit
ceaee7bbb3
18 changed files with 748 additions and 43 deletions
|
|
@ -152,7 +152,7 @@ describe("dropdownQuestion.vue", () => {
|
|||
const wrapper = shallowMount(dropdownQuestion, {
|
||||
propsData: {
|
||||
options: {},
|
||||
modelValue: 0,
|
||||
modelValue: "0",
|
||||
},
|
||||
mixins: [mockMixin]
|
||||
});
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ export default {
|
|||
disableAutoFill: Boolean,
|
||||
validationRules: String,
|
||||
cmsWidgetName: String,
|
||||
hasError: Boolean,
|
||||
errors: Array
|
||||
},
|
||||
setup(props) {
|
||||
const propsClone = Object.assign({}, props);
|
||||
|
|
|
|||
|
|
@ -1,25 +1,25 @@
|
|||
import siteHeader from "@/common-components/site-header/site-header";
|
||||
import { shallowMount } from "@vue/test-utils";
|
||||
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||
|
||||
describe("site-header", () => {
|
||||
|
||||
test("renders the logo image", () => {
|
||||
const wrapper = shallowMount(siteHeader, {
|
||||
setProps: {
|
||||
cmsWidgetName: "header",
|
||||
},
|
||||
mixins: [mockMixin]
|
||||
});
|
||||
const wrapper = setupMocks({mountOptionsMockData: {}});
|
||||
|
||||
|
||||
expect(wrapper.find("img")).toBeTruthy();
|
||||
wrapper.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
const mockMixin = {
|
||||
methods: {
|
||||
getCmsContent: jest.fn()
|
||||
}
|
||||
function setupMocks({
|
||||
mountOptionsMockData = {},
|
||||
}) {
|
||||
|
||||
const mountOptions = getMountOptions(mountOptionsMockData);
|
||||
const wrapper = shallowMount(siteHeader, mountOptions);
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,39 @@
|
|||
<template>
|
||||
<div class="site-header d-flex justify-content-center align-items-center flex-column">
|
||||
<img id="siteHeaderImage" class="img-fluid" :src="imageSrc" :alt="altText" />
|
||||
<menuModal/>
|
||||
<div>
|
||||
<div class="site-header d-flex justify-content-center align-items-center flex-column">
|
||||
<img id="siteHeaderImage" class="img-fluid" :src="imageSrc" :alt="altText" />
|
||||
<menuModal/>
|
||||
</div>
|
||||
<alert
|
||||
v-if="displayGlobalAlert"
|
||||
class="position-absolute rounded-0 w-100 border-0 shadow-sm start-0"
|
||||
cmsWidgetName="GlobalAlert"
|
||||
:manualHeadline="globalAlertMessage.messageHeadline"
|
||||
:manualCopy="globalAlertMessage.messageCopy"
|
||||
:alertClass="globalAlertMessage.type"
|
||||
v-bind:isDismissible="globalAlertMessage.isDismissible" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import menuModal from "@/common-components/site-header/menu-modal/menu-modal";
|
||||
import alert from "@/ux-components/alert/alert";
|
||||
import eventBus from "@/helpers/event-bus/event-bus";
|
||||
import { globalEvents } from "@/constants/events";
|
||||
|
||||
export default({
|
||||
name: "siteHeader",
|
||||
data() {
|
||||
return {
|
||||
displayGlobalAlert: false,
|
||||
globalAlertMessage: {
|
||||
isDismissible: false,
|
||||
messageCopy: "",
|
||||
messageHeadline: "",
|
||||
type: "",
|
||||
},
|
||||
};
|
||||
},
|
||||
props: {
|
||||
cmsWidgetName: String,
|
||||
},
|
||||
|
|
@ -22,8 +47,21 @@ import menuModal from "@/common-components/site-header/menu-modal/menu-modal";
|
|||
return this.getCmsContent(this.cmsWidgetName, "AltText")
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// Check if alert event is on the bus
|
||||
const alertEvent = eventBus.readAndPopEventFromBus(
|
||||
globalEvents.Categories.GLOBAL_ALERT,
|
||||
globalEvents.SubCategories.PAGE_NOT_FOUND
|
||||
);
|
||||
// If alert event is on the bus, then display the alert
|
||||
if (alertEvent !== undefined) {
|
||||
this.displayGlobalAlert = true;
|
||||
this.globalAlertMessage = alertEvent;
|
||||
}
|
||||
},
|
||||
components: {
|
||||
menuModal
|
||||
menuModal,
|
||||
alert
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ export default {
|
|||
validationRules: String,
|
||||
cmsWidgetName: String,
|
||||
maxLength: String,
|
||||
disableAutoFill: Boolean,
|
||||
},
|
||||
setup(props) {
|
||||
const propsClone = Object.assign({}, props);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { useMainStore } from "@/store";
|
||||
export default {
|
||||
name: "vehicle-banner",
|
||||
props: {
|
||||
|
|
@ -25,7 +24,7 @@ export default {
|
|||
return this.genericVehicleImage;
|
||||
}
|
||||
|
||||
const imageUrl = useMainStore().order.vehicle.imageUrl;
|
||||
const imageUrl = this.mainStore.order.vehicle.imageUrl;
|
||||
if (!imageUrl || imageUrl === "NULL"){
|
||||
return this.getUnmatchedVehicleIcon();
|
||||
}
|
||||
|
|
@ -53,7 +52,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
getUnmatchedVehicleIcon(){
|
||||
switch(useMainStore().order.vehicle.category){
|
||||
switch(this.mainStore.order.vehicle.category){
|
||||
case this.vehicleCategories.CAR:
|
||||
return this.carUnmatchedVehicleIcon;
|
||||
case this.vehicleCategories.SUV:
|
||||
|
|
|
|||
17
src/constants/events.js
Normal file
17
src/constants/events.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
const globalEvents = {
|
||||
Categories: {
|
||||
GLOBAL_ALERT: "GLOBAL_ALERT",
|
||||
},
|
||||
SubCategories: {
|
||||
PAGE_NOT_FOUND: "PAGE_NOT_FOUND",
|
||||
},
|
||||
};
|
||||
|
||||
const globalEventTypes = {
|
||||
Success: "alert-success",
|
||||
Warning: "alert-warning",
|
||||
Info: "alert-info",
|
||||
Danger: "alert-danger",
|
||||
};
|
||||
|
||||
export { globalEvents, globalEventTypes };
|
||||
|
|
@ -118,4 +118,36 @@ function mapStringToState(str) {
|
|||
}
|
||||
|
||||
return stringBuilder.trimStart();
|
||||
}
|
||||
}
|
||||
|
||||
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 !== "");
|
||||
}
|
||||
|
|
|
|||
35
src/helpers/event-bus/event-bus.js
Normal file
35
src/helpers/event-bus/event-bus.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { useMainStore } from '@/store';
|
||||
|
||||
export default {
|
||||
// Adds event to the bus given its category, subcategory, and eventValue;
|
||||
addEventToBus(category, subCategory, eventValue) {
|
||||
useMainStore().addEventToBus( {
|
||||
category: category,
|
||||
subCategory: subCategory,
|
||||
eventValue: eventValue,
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
// Finds event on the bus, removes the item, and returns its value to the caller.
|
||||
readAndPopEventFromBus(category, subCategory) {
|
||||
|
||||
const event = useMainStore().eventBusItem(category, subCategory);
|
||||
|
||||
if(event) {
|
||||
useMainStore().removeEventFromBus( {
|
||||
category: category,
|
||||
subCategory: subCategory,
|
||||
});
|
||||
}
|
||||
|
||||
return event;
|
||||
},
|
||||
|
||||
// Finds the event on the bus and returns its value to the caller, does not remove it.
|
||||
readEventFromBus(category, subCategory) {
|
||||
const event = useMainStore().eventBusItem(category, subCategory);
|
||||
|
||||
return event;
|
||||
},
|
||||
};
|
||||
78
src/helpers/event-bus/event-bus.spec.js
Normal file
78
src/helpers/event-bus/event-bus.spec.js
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import { globalEvents, globalEventTypes } from "@/constants/events";
|
||||
import eventBus from "@/helpers/event-bus/event-bus";
|
||||
import { useMainStore } from '@/store';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
|
||||
const pinia = createTestingPinia();
|
||||
useMainStore(pinia);
|
||||
|
||||
useMainStore().addEventToBus = jest.fn();
|
||||
useMainStore().removeEventFromBus = jest.fn();
|
||||
useMainStore().eventBusItem = jest.fn();
|
||||
|
||||
describe("event-bus.js", () => {
|
||||
let event = {
|
||||
isDismissible: true,
|
||||
messageCopy: "You can get a quote by starting on this page.",
|
||||
messageHeadline: "We're sorry, something went wrong.",
|
||||
type: globalEventTypes.Danger,
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
})
|
||||
|
||||
it("removes items when readandpop is called", () => {
|
||||
useMainStore().eventBusItem.mockReturnValueOnce(event);
|
||||
|
||||
const eventValue = eventBus.readAndPopEventFromBus(
|
||||
globalEvents.Categories.GLOBAL_ALERT,
|
||||
globalEvents.SubCategories.PAGE_NOT_FOUND
|
||||
);
|
||||
|
||||
expect(useMainStore().eventBusItem).toBeCalledTimes(1);
|
||||
expect(useMainStore().removeEventFromBus).toBeCalledTimes(1);
|
||||
|
||||
});
|
||||
|
||||
it("doesn't try to remove items when readandpop is called and item doesn't exist", () => {
|
||||
|
||||
useMainStore().eventBusItem.mockReturnValueOnce(undefined);
|
||||
|
||||
const eventValue = eventBus.readAndPopEventFromBus(
|
||||
globalEvents.Categories.GLOBAL_ALERT,
|
||||
globalEvents.SubCategories.PAGE_NOT_FOUND
|
||||
);
|
||||
|
||||
expect(useMainStore().eventBusItem).toBeCalledTimes(1);
|
||||
expect(useMainStore().removeEventFromBus).toBeCalledTimes(0);
|
||||
|
||||
});
|
||||
|
||||
it("returns event from bus", () => {
|
||||
|
||||
useMainStore().eventBusItem.mockReturnValueOnce(event);
|
||||
|
||||
const eventValue = eventBus.readEventFromBus(
|
||||
globalEvents.Categories.GLOBAL_ALERT,
|
||||
globalEvents.SubCategories.PAGE_NOT_FOUND
|
||||
);
|
||||
|
||||
expect(eventValue).toBe(event);
|
||||
|
||||
});
|
||||
|
||||
|
||||
it("Reads event from bus, should have event value.", () => {
|
||||
// Arrange / Act
|
||||
eventBus.addEventToBus(
|
||||
globalEvents.Categories.GLOBAL_ALERT,
|
||||
globalEvents.SubCategories.PAGE_NOT_FOUND,
|
||||
event
|
||||
);
|
||||
|
||||
expect(useMainStore().addEventToBus).toHaveBeenCalled();
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
404: Page Not Found
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'error-404'
|
||||
}
|
||||
</script>
|
||||
|
|
@ -117,13 +117,13 @@ function setupMocks({
|
|||
//Mock api responses
|
||||
const apiResponses = {
|
||||
cmsContent: {
|
||||
FunnelSubHeaderWidget: pageHeaderWidgetHeaderText,
|
||||
SiteSubHeaderWidget: pageHeaderWidgetHeaderText,
|
||||
VehicleModelQuestion: buttonQuestionContent,
|
||||
VehicleBannerWidget: {
|
||||
GenericVehicleImage:
|
||||
"https://digitalisscms.dev.safelite.io/images/default-source/default-album/blurred-image.jpg",
|
||||
},
|
||||
FunnelHeaderWidget: {
|
||||
SiteHeaderWidget: {
|
||||
LogoImage:
|
||||
"https://digitalisscms.dev.safelite.io/images/default-source/default-album/logos/insuranceLogo.jpg",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { lazyLoadComponent } from "@/router/dynamic-routing/component-loader";
|
|||
import {issPageValues} from '@/router/router-constants/issPage-values';
|
||||
import { routingTable } from "@/router/router-constants/routing-table";
|
||||
import { useMainStore } from '@/store';
|
||||
import eventBus from "@/helpers/event-bus/event-bus";
|
||||
import { globalEvents, globalEventTypes } from "@/constants/events";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
|
|
@ -141,13 +143,25 @@ function getNavigationMap (scenario, currentRoute) {
|
|||
};
|
||||
|
||||
function GoToStartOn404(next) {
|
||||
const errorPageName = issPageValues.ERROR_404;
|
||||
const errorPageName = issPageValues.VEHICLE_YEAR;
|
||||
router.addRoute({
|
||||
path: "/",
|
||||
name: errorPageName,
|
||||
component: lazyLoadComponent(errorPageName),
|
||||
});
|
||||
|
||||
// Put item on the bus
|
||||
eventBus.addEventToBus(
|
||||
globalEvents.Categories.GLOBAL_ALERT,
|
||||
globalEvents.SubCategories.PAGE_NOT_FOUND,
|
||||
{
|
||||
isDismissible: true,
|
||||
messageCopy: "You can get a quote by starting on this page.",
|
||||
messageHeadline: "We're sorry, something went wrong.",
|
||||
type: globalEventTypes.Danger,
|
||||
}
|
||||
);
|
||||
|
||||
next({
|
||||
name: errorPageName,
|
||||
query: {issPage: errorPageName },
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
export const issPageValues = {
|
||||
ERROR_404: "error-404",
|
||||
WELCOME_PAGE: "welcome-page",
|
||||
VEHICLE_MAKE: "vehicle-make",
|
||||
VEHICLE_YEAR: "vehicle-year",
|
||||
|
|
|
|||
|
|
@ -37,7 +37,8 @@ const getDefaultState = () => {
|
|||
crmCustomerId: null,
|
||||
lastPageVisited: null,
|
||||
experiments: [],
|
||||
triggeredSiteEntry: false
|
||||
triggeredSiteEntry: false,
|
||||
eventBus: []
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
@ -47,7 +48,17 @@ export const state = getDefaultState();
|
|||
export const useMainStore = defineStore({
|
||||
id: storeId,
|
||||
state: () => state,
|
||||
getters: {},
|
||||
getters: {
|
||||
eventBusItem: (state) => ( eventCategory, eventSubCategory) => {
|
||||
|
||||
const matchedEvent = state.applicationUser.eventBus.find(
|
||||
( { category, subCategory } ) =>
|
||||
category === eventCategory && subCategory === eventSubCategory
|
||||
);
|
||||
return matchedEvent?.eventValue;
|
||||
},
|
||||
eventBus: (state) => state.applicationUser.eventBus,
|
||||
},
|
||||
actions:
|
||||
{
|
||||
// Content API Actions
|
||||
|
|
@ -135,6 +146,21 @@ export const useMainStore = defineStore({
|
|||
this.order.vehicle.style = style;
|
||||
}
|
||||
},
|
||||
addEventToBus (event) {
|
||||
this.applicationUser.eventBus.push(event);
|
||||
},
|
||||
removeEventFromBus (eventData) {
|
||||
const matchedEvent = this.applicationUser.eventBus.find(
|
||||
( {category, subCategory } ) =>
|
||||
category === eventData.category && subCategory === eventData.subCategory
|
||||
);
|
||||
const itemIndex = this.applicationUser.eventBus.indexOf(matchedEvent);
|
||||
|
||||
// If the item exists, remove it.
|
||||
if (itemIndex > -1) {
|
||||
this.applicationUser.eventBus.splice(itemIndex, 1);
|
||||
}
|
||||
},
|
||||
|
||||
// populate initial state
|
||||
populateInitialState()
|
||||
|
|
|
|||
|
|
@ -3,19 +3,83 @@ import { createApp } from 'vue';
|
|||
import { createPinia } from "pinia";
|
||||
import App from '@/App.vue';
|
||||
|
||||
describe("Store", () => {
|
||||
let store = null;
|
||||
|
||||
beforeAll(() => {
|
||||
const vueApp = createApp(App);
|
||||
const pinia = createPinia();
|
||||
vueApp.use(pinia);
|
||||
describe("Store", () => {
|
||||
|
||||
let store;
|
||||
|
||||
const vueApp = createApp(App);
|
||||
const pinia = createPinia();
|
||||
vueApp.use(pinia);
|
||||
|
||||
beforeEach(() => {
|
||||
store = useMainStore();
|
||||
store.applicationUser.eventBus = [];
|
||||
jest.resetAllMocks();
|
||||
})
|
||||
|
||||
|
||||
it("Should Store Vehicle Year", () => {
|
||||
let testYear = "2001";
|
||||
store.updateVehicleYear(testYear);
|
||||
expect(store.order.vehicle.year).toEqual(testYear);
|
||||
});
|
||||
|
||||
it("Should add events to the bus", () => {
|
||||
let event = {
|
||||
category: "TestCategory",
|
||||
subCategory: "TestSubCategory",
|
||||
eventValue: {
|
||||
isDismissible: true,
|
||||
messageCopy: "You can get a quote by starting on this page.",
|
||||
messageHeadline: "We're sorry, something went wrong.",
|
||||
type: "testType",
|
||||
},
|
||||
};
|
||||
|
||||
store.addEventToBus(event);
|
||||
|
||||
expect(store.applicationUser.eventBus[0]).toEqual(event);
|
||||
});
|
||||
|
||||
it("Should remove events from the bus", () => {
|
||||
let event = {
|
||||
category: "TestCategory",
|
||||
subCategory: "TestSubCategory",
|
||||
eventValue: {
|
||||
isDismissible: true,
|
||||
messageCopy: "You can get a quote by starting on this page.",
|
||||
messageHeadline: "We're sorry, something went wrong.",
|
||||
type: "testType",
|
||||
},
|
||||
};
|
||||
|
||||
store.addEventToBus(event);
|
||||
|
||||
expect(store.applicationUser.eventBus.length).toBe(1);
|
||||
|
||||
store.removeEventFromBus({ category: event.category, subCategory: event.subCategory })
|
||||
|
||||
expect(store.applicationUser.eventBus.length).toBe(0);
|
||||
});
|
||||
|
||||
it("Should return correct event using the getter function eventBusItem", () => {
|
||||
let event = {
|
||||
category: "TestCategory",
|
||||
subCategory: "TestSubCategory",
|
||||
eventValue: {
|
||||
isDismissible: true,
|
||||
messageCopy: "You can get a quote by starting on this page.",
|
||||
messageHeadline: "We're sorry, something went wrong.",
|
||||
type: "testType",
|
||||
},
|
||||
};
|
||||
|
||||
store.addEventToBus(event);
|
||||
|
||||
const actual = store.eventBusItem(event.category, event.subCategory)
|
||||
|
||||
expect(actual).toEqual(event.eventValue);
|
||||
});
|
||||
|
||||
});
|
||||
222
src/ux-components/alert/alert.spec.js
Normal file
222
src/ux-components/alert/alert.spec.js
Normal file
|
|
@ -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 <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(() => 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;
|
||||
}
|
||||
189
src/ux-components/alert/alert.vue
Normal file
189
src/ux-components/alert/alert.vue
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
<template>
|
||||
<div
|
||||
class="alert fade show text-center mb-0 py-2 px-4"
|
||||
role="alert"
|
||||
:class="[
|
||||
isDismissible ? 'alert-dismissible' : '',
|
||||
this.alertClass,
|
||||
this.cssClassNameForCmsWidget,
|
||||
]">
|
||||
<p class="m-0 fw-bold alert-heading">{{ alertHeadline }}</p>
|
||||
<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 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
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
</p>
|
||||
</template>
|
||||
<button type="button" class="btn-close p-2" data-bs-dismiss="alert" aria-label="Close">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23.7 23.7" xml:space="preserve">
|
||||
<path
|
||||
d="m23.24 2.7-9.15 9.15L23.24 21a1.581 1.581 0 0 1-1.12 2.7c-.42 0-.82-.16-1.12-.46l-9.15-9.15-9.15 9.15c-.3.3-.7.46-1.12.46A1.581 1.581 0 0 1 .46 21l8.47-8.47.68-.68L.46 2.7c-.62-.62-.62-1.62 0-2.24.62-.62 1.62-.62 2.24 0l8.47 8.47.68.68L21 .46a1.57 1.57 0 0 1 2.23 0c.63.62.63 1.62.01 2.24z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
doesCopyContainRouterLink,
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
getRouterLinkRouteFromCopy,
|
||||
getRouterLinkDisplayTextFromCopy,
|
||||
splitCMSCopyOnParagraphTag,
|
||||
} from "@/helpers/cms-content-helper";
|
||||
|
||||
export default {
|
||||
name: "alert",
|
||||
props: {
|
||||
isDismissible: Boolean,
|
||||
/*
|
||||
alertClass class names:
|
||||
alert-success (green)
|
||||
alert-danger (red)
|
||||
alert-warning (yellow)
|
||||
alert-info (blue)
|
||||
*/
|
||||
alertClass: String,
|
||||
cmsWidgetName: {
|
||||
type: String,
|
||||
default(rawProps) {
|
||||
if (!rawProps.cmsWidgetName) {
|
||||
console.log("Error: Missing a CMS Widget Name (required field)");
|
||||
}
|
||||
return "widgetUndefined";
|
||||
},
|
||||
},
|
||||
manualHeadline: String,
|
||||
manualCopy: String,
|
||||
shouldScrollToOnMount: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
alertHeadline() {
|
||||
return this.manualHeadline
|
||||
? this.manualHeadline
|
||||
: this.getCmsContent(this.cmsWidgetName, "HeadlineText");
|
||||
},
|
||||
alertCopy() {
|
||||
return this.manualCopy
|
||||
? this.manualCopy
|
||||
: this.getCmsContent(this.cmsWidgetName, "BodyText");
|
||||
},
|
||||
splitAlertCopyForParagraphTag() {
|
||||
return splitCMSCopyOnParagraphTag(this.alertCopy);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
doesCopyContainRouterLink,
|
||||
splitCopyOnCMSPlaceHolder,
|
||||
getRouterLinkRouteFromCopy,
|
||||
getRouterLinkDisplayTextFromCopy,
|
||||
ensureAlertIsInViewPort() {
|
||||
if (this.shouldScrollToOnMount && this.$el.style.display != "none") {
|
||||
var footerHeight = this.getFooterInfoBoxHeight();
|
||||
if (!this.isAlertInViewport(footerHeight)) {
|
||||
this.$el.scrollIntoView(true); // 'true' attempts to scroll element to top of viewport
|
||||
}
|
||||
}
|
||||
},
|
||||
isAlertInViewport(footerHeight) {
|
||||
const rect = this.$el.getBoundingClientRect();
|
||||
return (
|
||||
rect.top >= 0 &&
|
||||
// remove footerHeight from window height to avoid items being hidden behind footer
|
||||
rect.bottom <=
|
||||
(window.innerHeight - footerHeight ||
|
||||
document.documentElement.clientHeight - footerHeight)
|
||||
);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.ensureAlertIsInViewPort();
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.alert {
|
||||
button {
|
||||
display: none;
|
||||
}
|
||||
.btn-close {
|
||||
background: none;
|
||||
opacity: 1;
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
}
|
||||
&.alert-dismissible {
|
||||
button {
|
||||
display: flex;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
}
|
||||
}
|
||||
&.alert-info {
|
||||
background-color: $blue-100;
|
||||
.alert-heading {
|
||||
color: $blue-700;
|
||||
}
|
||||
svg {
|
||||
fill: $blue-700;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
&.alert-danger {
|
||||
background-color: $red-100;
|
||||
.alert-heading {
|
||||
color: $red-600;
|
||||
}
|
||||
svg {
|
||||
fill: $red-600;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
&.alert-warning {
|
||||
background-color: $yellow-100;
|
||||
.alert-heading {
|
||||
color: $yellow-600;
|
||||
}
|
||||
svg {
|
||||
fill: $yellow-600;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
&.alert-success {
|
||||
background-color: $green-100;
|
||||
.alert-heading {
|
||||
color: $green-700;
|
||||
}
|
||||
svg {
|
||||
fill: $green-700;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
& p {
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0.25rem !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in a new issue