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, {
|
const wrapper = shallowMount(dropdownQuestion, {
|
||||||
propsData: {
|
propsData: {
|
||||||
options: {},
|
options: {},
|
||||||
modelValue: 0,
|
modelValue: "0",
|
||||||
},
|
},
|
||||||
mixins: [mockMixin]
|
mixins: [mockMixin]
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,8 @@ export default {
|
||||||
disableAutoFill: Boolean,
|
disableAutoFill: Boolean,
|
||||||
validationRules: String,
|
validationRules: String,
|
||||||
cmsWidgetName: String,
|
cmsWidgetName: String,
|
||||||
|
hasError: Boolean,
|
||||||
|
errors: Array
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const propsClone = Object.assign({}, props);
|
const propsClone = Object.assign({}, props);
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,25 @@
|
||||||
import siteHeader from "@/common-components/site-header/site-header";
|
import siteHeader from "@/common-components/site-header/site-header";
|
||||||
import { shallowMount } from "@vue/test-utils";
|
import { shallowMount } from "@vue/test-utils";
|
||||||
|
import { getMountOptions } from "@/helpers/unit-test-helper.js";
|
||||||
|
|
||||||
describe("site-header", () => {
|
describe("site-header", () => {
|
||||||
|
|
||||||
test("renders the logo image", () => {
|
test("renders the logo image", () => {
|
||||||
const wrapper = shallowMount(siteHeader, {
|
const wrapper = setupMocks({mountOptionsMockData: {}});
|
||||||
setProps: {
|
|
||||||
cmsWidgetName: "header",
|
|
||||||
},
|
|
||||||
mixins: [mockMixin]
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(wrapper.find("img")).toBeTruthy();
|
expect(wrapper.find("img")).toBeTruthy();
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockMixin = {
|
function setupMocks({
|
||||||
methods: {
|
mountOptionsMockData = {},
|
||||||
getCmsContent: jest.fn()
|
}) {
|
||||||
}
|
|
||||||
|
const mountOptions = getMountOptions(mountOptionsMockData);
|
||||||
|
const wrapper = shallowMount(siteHeader, mountOptions);
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,39 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="site-header d-flex justify-content-center align-items-center flex-column">
|
<div>
|
||||||
<img id="siteHeaderImage" class="img-fluid" :src="imageSrc" :alt="altText" />
|
<div class="site-header d-flex justify-content-center align-items-center flex-column">
|
||||||
<menuModal/>
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import menuModal from "@/common-components/site-header/menu-modal/menu-modal";
|
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({
|
export default({
|
||||||
name: "siteHeader",
|
name: "siteHeader",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
displayGlobalAlert: false,
|
||||||
|
globalAlertMessage: {
|
||||||
|
isDismissible: false,
|
||||||
|
messageCopy: "",
|
||||||
|
messageHeadline: "",
|
||||||
|
type: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
cmsWidgetName: String,
|
cmsWidgetName: String,
|
||||||
},
|
},
|
||||||
|
|
@ -22,8 +47,21 @@ import menuModal from "@/common-components/site-header/menu-modal/menu-modal";
|
||||||
return this.getCmsContent(this.cmsWidgetName, "AltText")
|
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: {
|
components: {
|
||||||
menuModal
|
menuModal,
|
||||||
|
alert
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ export default {
|
||||||
validationRules: String,
|
validationRules: String,
|
||||||
cmsWidgetName: String,
|
cmsWidgetName: String,
|
||||||
maxLength: String,
|
maxLength: String,
|
||||||
|
disableAutoFill: Boolean,
|
||||||
},
|
},
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const propsClone = Object.assign({}, props);
|
const propsClone = Object.assign({}, props);
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { useMainStore } from "@/store";
|
|
||||||
export default {
|
export default {
|
||||||
name: "vehicle-banner",
|
name: "vehicle-banner",
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -25,7 +24,7 @@ export default {
|
||||||
return this.genericVehicleImage;
|
return this.genericVehicleImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageUrl = useMainStore().order.vehicle.imageUrl;
|
const imageUrl = this.mainStore.order.vehicle.imageUrl;
|
||||||
if (!imageUrl || imageUrl === "NULL"){
|
if (!imageUrl || imageUrl === "NULL"){
|
||||||
return this.getUnmatchedVehicleIcon();
|
return this.getUnmatchedVehicleIcon();
|
||||||
}
|
}
|
||||||
|
|
@ -53,7 +52,7 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getUnmatchedVehicleIcon(){
|
getUnmatchedVehicleIcon(){
|
||||||
switch(useMainStore().order.vehicle.category){
|
switch(this.mainStore.order.vehicle.category){
|
||||||
case this.vehicleCategories.CAR:
|
case this.vehicleCategories.CAR:
|
||||||
return this.carUnmatchedVehicleIcon;
|
return this.carUnmatchedVehicleIcon;
|
||||||
case this.vehicleCategories.SUV:
|
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();
|
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
|
//Mock api responses
|
||||||
const apiResponses = {
|
const apiResponses = {
|
||||||
cmsContent: {
|
cmsContent: {
|
||||||
FunnelSubHeaderWidget: pageHeaderWidgetHeaderText,
|
SiteSubHeaderWidget: pageHeaderWidgetHeaderText,
|
||||||
VehicleModelQuestion: buttonQuestionContent,
|
VehicleModelQuestion: buttonQuestionContent,
|
||||||
VehicleBannerWidget: {
|
VehicleBannerWidget: {
|
||||||
GenericVehicleImage:
|
GenericVehicleImage:
|
||||||
"https://digitalisscms.dev.safelite.io/images/default-source/default-album/blurred-image.jpg",
|
"https://digitalisscms.dev.safelite.io/images/default-source/default-album/blurred-image.jpg",
|
||||||
},
|
},
|
||||||
FunnelHeaderWidget: {
|
SiteHeaderWidget: {
|
||||||
LogoImage:
|
LogoImage:
|
||||||
"https://digitalisscms.dev.safelite.io/images/default-source/default-album/logos/insuranceLogo.jpg",
|
"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 {issPageValues} from '@/router/router-constants/issPage-values';
|
||||||
import { routingTable } from "@/router/router-constants/routing-table";
|
import { routingTable } from "@/router/router-constants/routing-table";
|
||||||
import { useMainStore } from '@/store';
|
import { useMainStore } from '@/store';
|
||||||
|
import eventBus from "@/helpers/event-bus/event-bus";
|
||||||
|
import { globalEvents, globalEventTypes } from "@/constants/events";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
|
|
@ -141,13 +143,25 @@ function getNavigationMap (scenario, currentRoute) {
|
||||||
};
|
};
|
||||||
|
|
||||||
function GoToStartOn404(next) {
|
function GoToStartOn404(next) {
|
||||||
const errorPageName = issPageValues.ERROR_404;
|
const errorPageName = issPageValues.VEHICLE_YEAR;
|
||||||
router.addRoute({
|
router.addRoute({
|
||||||
path: "/",
|
path: "/",
|
||||||
name: errorPageName,
|
name: errorPageName,
|
||||||
component: lazyLoadComponent(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({
|
next({
|
||||||
name: errorPageName,
|
name: errorPageName,
|
||||||
query: {issPage: errorPageName },
|
query: {issPage: errorPageName },
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
export const issPageValues = {
|
export const issPageValues = {
|
||||||
ERROR_404: "error-404",
|
|
||||||
WELCOME_PAGE: "welcome-page",
|
WELCOME_PAGE: "welcome-page",
|
||||||
VEHICLE_MAKE: "vehicle-make",
|
VEHICLE_MAKE: "vehicle-make",
|
||||||
VEHICLE_YEAR: "vehicle-year",
|
VEHICLE_YEAR: "vehicle-year",
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,8 @@ const getDefaultState = () => {
|
||||||
crmCustomerId: null,
|
crmCustomerId: null,
|
||||||
lastPageVisited: null,
|
lastPageVisited: null,
|
||||||
experiments: [],
|
experiments: [],
|
||||||
triggeredSiteEntry: false
|
triggeredSiteEntry: false,
|
||||||
|
eventBus: []
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -47,7 +48,17 @@ export const state = getDefaultState();
|
||||||
export const useMainStore = defineStore({
|
export const useMainStore = defineStore({
|
||||||
id: storeId,
|
id: storeId,
|
||||||
state: () => state,
|
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:
|
actions:
|
||||||
{
|
{
|
||||||
// Content API Actions
|
// Content API Actions
|
||||||
|
|
@ -135,6 +146,21 @@ export const useMainStore = defineStore({
|
||||||
this.order.vehicle.style = style;
|
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
|
// populate initial state
|
||||||
populateInitialState()
|
populateInitialState()
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,83 @@ import { createApp } from 'vue';
|
||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
import App from '@/App.vue';
|
import App from '@/App.vue';
|
||||||
|
|
||||||
describe("Store", () => {
|
|
||||||
let store = null;
|
|
||||||
|
|
||||||
beforeAll(() => {
|
describe("Store", () => {
|
||||||
const vueApp = createApp(App);
|
|
||||||
const pinia = createPinia();
|
let store;
|
||||||
vueApp.use(pinia);
|
|
||||||
|
const vueApp = createApp(App);
|
||||||
|
const pinia = createPinia();
|
||||||
|
vueApp.use(pinia);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
store = useMainStore();
|
store = useMainStore();
|
||||||
|
store.applicationUser.eventBus = [];
|
||||||
|
jest.resetAllMocks();
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
it("Should Store Vehicle Year", () => {
|
it("Should Store Vehicle Year", () => {
|
||||||
let testYear = "2001";
|
let testYear = "2001";
|
||||||
store.updateVehicleYear(testYear);
|
store.updateVehicleYear(testYear);
|
||||||
expect(store.order.vehicle.year).toEqual(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